Consolidate Code for Each Case in Enum


(Karim Nassar) #1

Although I appreciate the intent, I too find the proposed sugar far more noisy and harder to read than the more typical approach.

But:

TLDR; Simplifying the accessing of enum associated values would be worthy of some syntactic sugar in my opinion.

One area of enums that I’d love to see some sugar wrapped around (and perhaps this has already been discussed previously?) is extracting associated values.

There are many times where, given an enum like:

enum Feedback {
  case ok
  case info(String)
  case warning(String, Location)
  case error(String, Location)
}

I’d love it if we could tag the associated values with some semantic accessor, perhaps borrowed from tuples:

enum Feedback {
  case ok
  case info(msg: String)
  case warning(msg: String, loc: Location)
  case error(msg: String, loc: Location)
}

then:

let foo = self.getSomeFeedback() // -> Feedback
if let msg = foo.msg { // since not all cases can hold a ‘msg’ .msg is an Optional
  print(foo)
}

I know this is a trivial example, since there are only 4 cases, but I have enums with many more cases where many of the cases share some common semantic themes, and producing large switch statements to exercise all of the possible patterns that might hold a given associated value can be verbose to the point of illegibility.

I know there is also the (IMHO terrible) "if let case” incantation, but I can never remember how it is spelled, and its even harder to read, IMHO.

—Karim

···

I don't think I've ever wanted to distribute the patterns of a switch statement across multiple files. It seems like you want an enum of enums if the code you're writing needs this kind of chunking. Distributing cases is also far more brittle than the existing local switch; failing to include a file in the build that covers necessary cases now becomes a module-level error rather than a statement-local one. Finally, the proposal seems to do the opposite of consolidate and simplify code by introducing quite a lot of syntax around what it aims to do, and by muddying the meaning of a keyword that was previously only meant for types.

A few conceptual questions:

How does this interact with versioned cases?
What about enums that carry values?

~Robert Widmann

2017/01/06 23:59、Tim Shadel via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> のメッセージ:

Idea: Consolidate the Code for Each Case in an Enum

# Motivation:

Consolidate all code related to a single enum case in one spot. This makes it easier to ensure that all the pieces mesh coherently across that one case.

# Background:

Enum cases _feel_ like separately defined, but tightly related structs because each case can have distinct associated values. They have special privileges that a family of structs doesn't have, like `self = .otherCase`. Enums are really awesome.

# Proposed Solution:

Any `func` or dynamic `var` that provides a unique response per `case` uses a `switch` to do so. I propose to hide that standard `switch` behind some syntactic sugar. Possibly `extension MyEnum.myCase`, assuming that nothing extra is allowed there (protocol conformance, generic constraints, etc.).

Here's a typical example of a (simplified) enum that represents 2 states, and conforms to 2 protocols, each requiring different dynamic values based on the case of the enum. In both places, an outer `switch` is used to select the current enum case, and the logic within each branch further determines the value returned.

protocol State {
   mutating func react(to event: Event)
}

enum TokenState: State, CustomStringConvertible {

   case expired(at: Date)
   case validated(token: String)

   var description: String {
     switch self {
       case let .expired(at):
           return "Expired at \(at)"
       case let .validated(token):
           return "Token \(token) has been validated."
     }
   }

   mutating func react(to event: Event) {
       switch self {
       case .expired:
           switch event {
           case _ as TokenRefreshed:
               self = .validated(token: event.token)
           default:
               break
           }
       case .validated:
           switch event {
           case _ as TokenRejected:
               self = .expired(at: Date())
           case _ as UserLoggedOut:
               self = .expired(at: Date())
           default:
               break
           }
       }
   }

}

If we instead allow all the code for each enum case to be consolidated, this new code looks much more like the rest of the code we write in Swift. Real world enums frequently have many more cases, and as the number of enum cases grows consolidating all their logic is increasingly helpful. The following proposal is identical to the code above, it simply "hides" the outer switch statement of each value.

enum TokenState: State, CustomStringConvertible {
   case expired(at: Date)
   case validated(token: String)
}

extension TokenState.expired {

   var description: String {
     return "Token expired at \(self.at <http://self.at/>)"
   }

   mutating func react(to event: Event) {
       switch event {
       case _ as TokenRefreshed:
           self = .untested(token: event.token)
       default:
           break
       }
   }

}

extension TokenState.validated {

   var description: String {
     return "Token \(self.token) has been validated."
   }

   mutating func react(to event: Event) {
       switch event {
       case _ as TokenRejected:
           self = .expired(at: Date())
       case _ as UserLoggedOut:
           self = .expired(at: Date())
       default:
           break
       }
   }

}

I've also shown automatic binding of each case's associated values to properties available on `self` ... but maybe it's better if they're bound to variable references captured the way a closure does. I'm not an expert in this part.

Back to the meat of the idea, what happens when a case isn't extended, or only partially extended? Because it's simply a fancy `switch`, it still must be exhaustive or provide a `default` branch.

extension TokenState.expired {

   var description: String {
     return "Token expired at \(self.at <http://self.at/>)"
   }

   <<< error: react(to:) must be exhaustively defined. Missing implementation for case .expired
}

Can be mitigated with:

enum TokenState: State, CustomStringConvertible {
   case expired(at: Date)
   case validated(token: String)

   // This becomes the `default` branch in the generated `switch`
   mutating func react(to event: Event) {
       print("Ignoring \(event) in case \(self)")
   }
}

Note that this implementation for the `default` branch is just that. This is not creating a superclass/subclass relationship between the `enum` and the `case`, it's merely a convenient way to construct a `switch` statement. I'm not proposing to deprecate any existing source, merely introduce a more convenient form of a very typical pattern, so I hope it is source-compatible by the definition you guys are using.

Thoughts?

--Tim
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Karim Nassar) #2

Although I appreciate the intent, I too find the proposed sugar far more noisy and harder to read than the more typical approach.

But:

TLDR; Simplifying the accessing of enum associated values would be worthy of some syntactic sugar in my opinion.

One area of enums that I’d love to see some sugar wrapped around (and perhaps this has already been discussed previously?) is extracting associated values.

There are many times where, given an enum like:

enum Feedback {
  case ok
  case info(String)
  case warning(String, Location)
  case error(String, Location)
}

I’d love it if we could tag the associated values with some semantic accessor, perhaps borrowed from tuples:

enum Feedback {
  case ok
  case info(msg: String)
  case warning(msg: String, loc: Location)
  case error(msg: String, loc: Location)
}

then:

let foo = self.getSomeFeedback() // -> Feedback
if let msg = foo.msg { // since not all cases can hold a ‘msg’ .msg is an Optional
  print(foo)
}

I know this is a trivial example, since there are only 4 cases, but I have enums with many more cases where many of the cases share some common semantic themes, and producing large switch statements to exercise all of the possible patterns that might hold a given associated value can be verbose to the point of illegibility.

I know there is also the (IMHO terrible) "if let case” incantation, but I can never remember how it is spelled, and its even harder to read, IMHO.

—Karim

···

I don't think I've ever wanted to distribute the patterns of a switch statement across multiple files. It seems like you want an enum of enums if the code you're writing needs this kind of chunking. Distributing cases is also far more brittle than the existing local switch; failing to include a file in the build that covers necessary cases now becomes a module-level error rather than a statement-local one. Finally, the proposal seems to do the opposite of consolidate and simplify code by introducing quite a lot of syntax around what it aims to do, and by muddying the meaning of a keyword that was previously only meant for types.

A few conceptual questions:

How does this interact with versioned cases?
What about enums that carry values?

~Robert Widmann

2017/01/06 23:59、Tim Shadel via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> のメッセージ:

Idea: Consolidate the Code for Each Case in an Enum

# Motivation:

Consolidate all code related to a single enum case in one spot. This makes it easier to ensure that all the pieces mesh coherently across that one case.

# Background:

Enum cases _feel_ like separately defined, but tightly related structs because each case can have distinct associated values. They have special privileges that a family of structs doesn't have, like `self = .otherCase`. Enums are really awesome.

# Proposed Solution:

Any `func` or dynamic `var` that provides a unique response per `case` uses a `switch` to do so. I propose to hide that standard `switch` behind some syntactic sugar. Possibly `extension MyEnum.myCase`, assuming that nothing extra is allowed there (protocol conformance, generic constraints, etc.).

Here's a typical example of a (simplified) enum that represents 2 states, and conforms to 2 protocols, each requiring different dynamic values based on the case of the enum. In both places, an outer `switch` is used to select the current enum case, and the logic within each branch further determines the value returned.

protocol State {
   mutating func react(to event: Event)
}

enum TokenState: State, CustomStringConvertible {

   case expired(at: Date)
   case validated(token: String)

   var description: String {
     switch self {
       case let .expired(at):
           return "Expired at \(at)"
       case let .validated(token):
           return "Token \(token) has been validated."
     }
   }

   mutating func react(to event: Event) {
       switch self {
       case .expired:
           switch event {
           case _ as TokenRefreshed:
               self = .validated(token: event.token)
           default:
               break
           }
       case .validated:
           switch event {
           case _ as TokenRejected:
               self = .expired(at: Date())
           case _ as UserLoggedOut:
               self = .expired(at: Date())
           default:
               break
           }
       }
   }

}

If we instead allow all the code for each enum case to be consolidated, this new code looks much more like the rest of the code we write in Swift. Real world enums frequently have many more cases, and as the number of enum cases grows consolidating all their logic is increasingly helpful. The following proposal is identical to the code above, it simply "hides" the outer switch statement of each value.

enum TokenState: State, CustomStringConvertible {
   case expired(at: Date)
   case validated(token: String)
}

extension TokenState.expired {

   var description: String {
     return "Token expired at \(self.at <http://self.at/>)"
   }

   mutating func react(to event: Event) {
       switch event {
       case _ as TokenRefreshed:
           self = .untested(token: event.token)
       default:
           break
       }
   }

}

extension TokenState.validated {

   var description: String {
     return "Token \(self.token) has been validated."
   }

   mutating func react(to event: Event) {
       switch event {
       case _ as TokenRejected:
           self = .expired(at: Date())
       case _ as UserLoggedOut:
           self = .expired(at: Date())
       default:
           break
       }
   }

}

I've also shown automatic binding of each case's associated values to properties available on `self` ... but maybe it's better if they're bound to variable references captured the way a closure does. I'm not an expert in this part.

Back to the meat of the idea, what happens when a case isn't extended, or only partially extended? Because it's simply a fancy `switch`, it still must be exhaustive or provide a `default` branch.

extension TokenState.expired {

   var description: String {
     return "Token expired at \(self.at <http://self.at/>)"
   }

   <<< error: react(to:) must be exhaustively defined. Missing implementation for case .expired
}

Can be mitigated with:

enum TokenState: State, CustomStringConvertible {
   case expired(at: Date)
   case validated(token: String)

   // This becomes the `default` branch in the generated `switch`
   mutating func react(to event: Event) {
       print("Ignoring \(event) in case \(self)")
   }
}

Note that this implementation for the `default` branch is just that. This is not creating a superclass/subclass relationship between the `enum` and the `case`, it's merely a convenient way to construct a `switch` statement. I'm not proposing to deprecate any existing source, merely introduce a more convenient form of a very typical pattern, so I hope it is source-compatible by the definition you guys are using.

Thoughts?

--Tim
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(David Sweeris) #3

Can't remember if it's come up before, but +1. I can't count how many times I've written something like:
enum Foo : CustomStringConvertible {
    case c1(T1)
    case c2(T2)
    ...
    case cN(TN)

    var description: String {
        switch self {
            case .c1(let val): return "\(val)"
            case .c2(let val): return "\(val)"
            ...
            case .cN(let val): return "\(val)"
        }
    }
}

Being able to simplify that to:
var description: String {
    let nilDesc = "some appropriate description"
    return "\(self.0 ?? nilDesc)"
}

Would be great.

- Dave Sweeris

···

On Jan 8, 2017, at 06:53, Karim Nassar via swift-evolution <swift-evolution@swift.org> wrote:

One area of enums that I’d love to see some sugar wrapped around (and perhaps this has already been discussed previously?) is extracting associated values.

There are many times where, given an enum like:

enum Feedback {
  case ok
  case info(String)
  case warning(String, Location)
  case error(String, Location)
}

I’d love it if we could tag the associated values with some semantic accessor, perhaps borrowed from tuples:

enum Feedback {
  case ok
  case info(msg: String)
  case warning(msg: String, loc: Location)
  case error(msg: String, loc: Location)
}

then:

let foo = self.getSomeFeedback() // -> Feedback
if let msg = foo.msg { // since not all cases can hold a ‘msg’ .msg is an Optional
  print(foo)
}


(Derrick Ho) #4

Currently we can write a helper method to aid in getting the values inside
the enum associated value. Below is a fully working implementation:


enum Package {

case box(String, Int)

case circular(String)

var associated: Associated {

return Associated(package: self)

}

struct Associated {

let box: (String, Int)?

let circular: (String)?

init(package: Package) {

switch package {

case .box(let b):

box = b

circular = nil

case .circular(let b):

box = nil

circular = b

}

}

}

}

let b = Package.box("square", 5)

b.associated.box?.0 // Optional("square")

b.associated.box?.1 // Optional(5)

b.associated.circular // nil

let c = Package.circular("round")

c.associated.box?.0 // nil

c.associated.box?.1 // nil

c.associated.circular // Optional("round")

I had to wedge in a special type called "Associated" and had to write some
boiler-plate code to get this effect. It is quite predictable and can
probably be done under the hood. I would of course prefer syntactic sugar
to simplify it and turn

b.associated.box?.0

into

b.box?.0
···

On Sun, Jan 8, 2017 at 1:05 PM David Sweeris via swift-evolution < swift-evolution@swift.org> wrote:

On Jan 8, 2017, at 06:53, Karim Nassar via swift-evolution < > swift-evolution@swift.org> wrote:

One area of enums that I’d love to see some sugar wrapped around (and
perhaps this has already been discussed previously?) is extracting
associated values.

There are many times where, given an enum like:

enum Feedback {
case ok
case info(String)
case warning(String, Location)
case error(String, Location)
}

I’d love it if we could tag the associated values with some semantic
accessor, perhaps borrowed from tuples:

enum Feedback {
case ok
case info(msg: String)
case warning(msg: String, loc: Location)
case error(msg: String, loc: Location)
}

then:

let foo = self.getSomeFeedback() // -> Feedback
if let msg = foo.msg { // since not all cases can hold a ‘msg’ .msg is an
Optional
print(foo)
}

Can't remember if it's come up before, but +1. I can't count how many
times I've written something like:
enum Foo : CustomStringConvertible {
    case c1(T1)
    case c2(T2)
    ...
    case cN(TN)

    var description: String {
        switch self {
            case .c1(let val): return "\(val)"
            case .c2(let val): return "\(val)"
            ...
            case .cN(let val): return "\(val)"
        }
    }
}

Being able to simplify that to:
var description: String {
    let nilDesc = "some appropriate description"
    return "\(self.0 ?? nilDesc)"
}

Would be great.

- Dave Sweeris
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Tim Shadel) #5

It seems like my original (simplified) examples didn't clearly highlight what the problem I'm focused on solving.

Enums get large, and they get complicated because the code for each case gets sliced up and scattered across many functions. It becomes a "one of these things is not like the other" situation because writing functions inside enums is unlike writing functions in any other part of Swift code.

Also, to be clear, my goal is _not_ code brevity. It is coherence, the property where related code is located together. Some increase in code verbosity is acceptable to make code more coherent, since that leads to long-term maintainability.

I've pulled out a few of the larger enums I've seen in code to try to illustrate this. Along the way, I've made a few alterations based on the comments I've seen come through. (Rien & Robert: I've pushed the definitions inside the declaration, to ensure they don't end up in other files since that was never my intention; Rien & Daniel: I've altered the syntax to open a brace right after the case declaration making everything outside it the default, and I like that better).

Here's an enum used to consolidate the descriptions of UI elements for a screen, allowing the datasource to alter their order, presentation, and visibility while keeping the set of possible fields clean and finite. These enum values are used much like singletons.

enum OneOnOneField: Int {
    case agenda
    case summary
    case date
    case notes

    struct Info {
        var title: String
        var placeholder: String
        var image: UIImage
    }

    var info: Info {
        switch self {
        case .agenda:
            return Info(
                title: NSLocalizedString("Agenda", comment: "One on one field header"),
                placeholder: NSLocalizedString("Add an agenda", comment: "One on one field placeholder"),
                image: #imageLiteral(resourceName: "Agenda-Small"))
        case .summary:
            return Info(
                title: NSLocalizedString("Summary", comment: "One on one field header"),
                placeholder: NSLocalizedString("Add a summary", comment: "One on one field placeholder"),
                image: #imageLiteral(resourceName: "Summary-Small"))
        case .date:
            return Info(title: "", placeholder: "", image: UIImage())
        case .notes:
            return Info(title: NSLocalizedString("Personal Notes", comment: "Title for personal notes screen"), placeholder: "", image: UIImage())
        }
    }
}
Consolidating them could instead look something like this:

enum OneOnOneField: Int {

    var title: String { return "" }
    var placeholder: String { return "" }
    var image: UIImage { return UIImage() }

    case agenda {
        var title: String { return NSLocalizedString("Agenda", comment: "One on one field header") }
        var placeholder: String { return NSLocalizedString("Add an agenda", comment: "One on one field placeholder") }
        var image: UIImage { return #imageLiteral(resourceName: "Agenda-Small") }
    }

    case summary {
        var title: String { return NSLocalizedString("Summary", comment: "One on one field header") }
        var placeholder: String { return NSLocalizedString("Add a summary", comment: "One on one field placeholder") }
        var image: UIImage { return #imageLiteral(resourceName: "Summary-Small") }
    }

    case date

    case notes {
        var title: String { return NSLocalizedString("Personal Notes", comment: "Title for personal notes screen") }
    }

}
Here's an enum that implements the basics of a state machine for OAuth 2 token use, refreshing, and login. Some of its cases have associated values and some don't.

enum TokenState: State {

    case loading
    case none
    case expired(Date)
    case untested(token: String)
    case validated(token: String)

    var description: String {
        switch self {
        case .loading:
            return "Loading token from disk"
        case .none:
            return "No token found"
        case let .expired(at):
            return "Expired at \(at)"
        case let .untested(token):
            return "Received token \(token), but it hasn't been tested."
        case let .validated(token):
            return "Token \(token) has been validated."
        }
    }

    mutating func react(to event: Event) {
        switch self {
        case .loading:
            switch event {
            case _ as TokenNotFound:
                self = .none
            case let expired as TokenExpired:
                self = .expired(expired.at)
            case let loaded as TokenLoaded:
                self = .untested(token: loaded.token)
            default:
                break
            }
        case .none:
            switch event {
            case let loggedIn as UserLoggedIn:
                self = .untested(token: loggedIn.token)
            default:
                break
            }
        case .expired:
            switch event {
            case let refreshed as TokenRefreshed:
                self = .untested(token: refreshed.token)
            case _ as TokenRefreshErrored:
                self = .none
            default:
                break
            }
        case let .untested(token):
            switch event {
            case _ as UserLoaded:
                self = .validated(token: token)
            case _ as TokenRejected:
                self = .expired(at: Date())
            default:
                break
            }
        case .validated:
            switch event {
            case _ as TokenRejected:
                self = .expired(Date())
            case _ as UserLoggedOut:
                self = .none
            default:
                break
            }
        }
    }

    static var initialState: TokenState {
        return .loading
    }

}
After consolidation, this becomes:

enum TokenState: State {

    static var initialState: TokenState {
        return .loading
    }

    case loading {
        var description: String {
            return "Loading token from disk"
        }

        mutating func react(to event: Event) {
            switch event {
            case _ as TokenNotFound:
                self = .none
            case let expired as TokenExpired:
                self = .expired(at: expired.at)
            case let loaded as TokenLoaded:
                self = .untested(token: loaded.token)
            default:
                break
            }
        }
    }

    case none {
        var description: String {
            return "No token found"
        }

        mutating func react(to event: Event) {
            switch event {
            case let loggedIn as UserLoggedIn:
                self = .untested(token: loggedIn.token)
            default:
                break
            }
        }
    }

    case expired(at: Date) {
        var description: String {
            return "Expired at \(at)"
        }

        mutating func react(to event: Event) {
            switch event {
            case let refreshed as TokenRefreshed:
                self = .untested(token: refreshed.token)
            case _ as TokenRefreshErrored:
                self = .none
            default:
                break
            }
        }
    }

    case untested(token: String) {
        var description: String {
            return "Received token \(token), but it hasn't been tested."
        }

        mutating func react(to event: Event) {
            switch event {
            case _ as UserLoaded:
                self = .validated(token: token)
            case _ as TokenRejected:
                self = .expired(at: Date())
            default:
                break
            }
        }
    }

    case validated(token: String) {
        var description: String {
            return "Token \(token) has been validated."
        }

        mutating func react(to event: Event) {
            switch event {
            case _ as TokenRejected:
                self = .expired(at: Date())
            case _ as UserLoggedOut:
                self = .none
            default:
                break
            }
        }
    }

}

···

On Jan 8, 2017, at 12:22 PM, Derrick Ho via swift-evolution <swift-evolution@swift.org> wrote:

Currently we can write a helper method to aid in getting the values inside the enum associated value. Below is a fully working implementation:

enum Package {
	case box(String, Int)
	case circular(String)
	
	var associated: Associated {
		return Associated(package: self)
	}
	
	struct Associated {
		let box: (String, Int)?
		let circular: (String)?
		init(package: Package) {
			switch package {
			case .box(let b):
				box = b
				circular = nil
			case .circular(let b):
				box = nil
				circular = b
			}
		}
	}
}

let b = Package.box("square", 5)
b.associated.box?.0 // Optional("square")
b.associated.box?.1 // Optional(5)
b.associated.circular // nil

let c = Package.circular("round")
c.associated.box?.0 // nil
c.associated.box?.1 // nil
c.associated.circular // Optional("round")

I had to wedge in a special type called "Associated" and had to write some boiler-plate code to get this effect. It is quite predictable and can probably be done under the hood. I would of course prefer syntactic sugar to simplify it and turn

b.associated.box?.0

into

b.box?.0

On Sun, Jan 8, 2017 at 1:05 PM David Sweeris via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Jan 8, 2017, at 06:53, Karim Nassar via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

One area of enums that I’d love to see some sugar wrapped around (and perhaps this has already been discussed previously?) is extracting associated values.

There are many times where, given an enum like:

enum Feedback {
  case ok
  case info(String)
  case warning(String, Location)
  case error(String, Location)
}

I’d love it if we could tag the associated values with some semantic accessor, perhaps borrowed from tuples:

enum Feedback {
  case ok
  case info(msg: String)
  case warning(msg: String, loc: Location)
  case error(msg: String, loc: Location)
}

then:

let foo = self.getSomeFeedback() // -> Feedback
if let msg = foo.msg { // since not all cases can hold a ‘msg’ .msg is an Optional
  print(foo)
}

Can't remember if it's come up before, but +1. I can't count how many times I've written something like:
enum Foo : CustomStringConvertible {
    case c1(T1)
    case c2(T2)
    ...
    case cN(TN)

    var description: String {
        switch self {
            case .c1(let val): return "\(val)"
            case .c2(let val): return "\(val)"
            ...
            case .cN(let val): return "\(val)"
        }
    }
}

Being able to simplify that to:
var description: String {
    let nilDesc = "some appropriate description"
    return "\(self.0 ?? nilDesc)"
}

Would be great.

- Dave Sweeris
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Sean Heber) #6

I would like something along these lines. I would suggest going farther and do something like this if possible to avoid repeating the type information all over the place:

enum OneOnOneField: Int {
  let title: String
  let placeholder: String
  let image: UIImage

  case agenda {
          title: NSLocalizedString("Agenda", comment: "One on one field header")
          placeholder: NSLocalizedString("Add an agenda", comment: "One on one field placeholder”)
    image: #imageLiteral(resourceName: "Agenda-Small”)
  }

  // etc
}

l8r
Sean

···

On Jan 9, 2017, at 11:54 AM, Tim Shadel via swift-evolution <swift-evolution@swift.org> wrote:

It seems like my original (simplified) examples didn't clearly highlight what the problem I'm focused on solving.

Enums get large, and they get complicated because the code for each case gets sliced up and scattered across many functions. It becomes a "one of these things is not like the other" situation because writing functions inside enums is unlike writing functions in any other part of Swift code.

Also, to be clear, my goal is _not_ code brevity. It is coherence, the property where related code is located together. Some increase in code verbosity is acceptable to make code more coherent, since that leads to long-term maintainability.

I've pulled out a few of the larger enums I've seen in code to try to illustrate this. Along the way, I've made a few alterations based on the comments I've seen come through. (Rien & Robert: I've pushed the definitions inside the declaration, to ensure they don't end up in other files since that was never my intention; Rien & Daniel: I've altered the syntax to open a brace right after the case declaration making everything outside it the default, and I like that better).

Here's an enum used to consolidate the descriptions of UI elements for a screen, allowing the datasource to alter their order, presentation, and visibility while keeping the set of possible fields clean and finite. These enum values are used much like singletons.

enum OneOnOneField: Int {

case agenda
    case summary
    case date
    case notes

    struct Info {

        var title
: String

        var placeholder
: String

        var image
:
UIImage
    
}

    var info
: Info {

switch self {

case .agenda:

return Info(

                title
: NSLocalizedString("Agenda", comment: "One on one field header"),

                placeholder
: NSLocalizedString("Add an agenda", comment: "One on one field placeholder"),

                image
: #imageLiteral(resourceName: "Agenda-Small"))

case .summary:

return Info(

                title
: NSLocalizedString("Summary", comment: "One on one field header"),

                placeholder
: NSLocalizedString("Add a summary", comment: "One on one field placeholder"),

                image
: #imageLiteral(resourceName: "Summary-Small"))

case .date:

return Info(title: "", placeholder: "", image: UIImage())

case .notes:

return Info(title: NSLocalizedString("Personal Notes", comment: "Title for personal notes screen"), placeholder: "", image: UIImage())

}

}
}
Consolidating them could instead look something like this:

enum OneOnOneField: Int {

    var title
: String { return "" }

    var placeholder
: String { return "" }

    var image
: UIImage { return UIImage() }

case agenda {
        var title: String { return NSLocalizedString("Agenda", comment: "One on one field header") }

        var placeholder
: String { return NSLocalizedString("Add an agenda", comment: "One on one field placeholder") }

        var image
: UIImage { return #imageLiteral(resourceName: "Agenda-Small") }

}

case summary {
        var title: String { return NSLocalizedString("Summary", comment: "One on one field header") }

        var placeholder
: String { return NSLocalizedString("Add a summary", comment: "One on one field placeholder") }

        var image
: UIImage { return #imageLiteral(resourceName: "Summary-Small") }

}

case date

    case notes {
        var title: String { return NSLocalizedString("Personal Notes", comment: "Title for personal notes screen") }

}

}
Here's an enum that implements the basics of a state machine for OAuth 2 token use, refreshing, and login. Some of its cases have associated values and some don't.

enum TokenState: State {

case loading
    case none
    case expired(Date)
    case untested(token: String)
    case validated(token: String)

    var description: String {

switch self {

case .loading:

return "Loading token from disk"

case .none:

return "No token found"

case let .expired(at):

return "Expired at \(at)"

case let .untested(token):

return "Received token \(token), but it hasn't been tested."

case let .validated(token):

return "Token \(token) has been validated."

}

}

    mutating func react
(to event: Event) {

switch self {

case .loading:

switch event {

case _ as TokenNotFound:

                self
= .
none
            
case let expired as TokenExpired:

                self
= .expired(expired.at)

case let loaded as TokenLoaded:

                self
= .untested(token: loaded.token)
            default:

break

}

case .none:

switch event {

case let loggedIn as UserLoggedIn:

                self
= .untested(token: loggedIn.token)
            default:

break

}

case .expired:

switch event {

case let refreshed as TokenRefreshed:

                self
= .untested(token: refreshed.token)

case _ as TokenRefreshErrored:

                self
= .
none

            default:

break

}

case let .untested(token):

switch event {

case _ as UserLoaded:

                self
= .validated(token: token)

case _ as TokenRejected:

                self
= .expired(at: Date())
            default:

break

}

case .validated:

switch event {

case _ as TokenRejected:

                self
= .expired(Date())

case _ as UserLoggedOut:

                self
= .
none

            default:

break

}

}

}

static var initialState: TokenState {

return .
loading
    
}

}
After consolidation, this becomes:

enum TokenState: State {

static var initialState: TokenState {

return .
loading
    
}

case loading {
        var description: String {

return "Loading token from disk"

}

        mutating func react
(to event: Event) {

switch event {

case _ as TokenNotFound:

                self
= .
none
            
case let expired as TokenExpired:

                self
= .expired(at: expired.at)

case let loaded as TokenLoaded:

                self
= .untested(token: loaded.token)
            default:

break

}

}

}

case none {
        var description: String {

return "No token found"

}

        mutating func react
(to event: Event) {

switch event {

case let loggedIn as UserLoggedIn:

                self
= .untested(token: loggedIn.token)
            default:

break

}

}

}

case expired(at: Date) {
        var description: String {

return "Expired at \(at)"

}

        mutating func react
(to event: Event) {

switch event {

case let refreshed as TokenRefreshed:

                self
= .untested(token: refreshed.token)

case _ as TokenRefreshErrored:

                self
= .
none

            default:

break

}

}

}

case untested(token: String) {
        var description: String {

return "Received token \(token), but it hasn't been tested."

}

        mutating func react
(to event: Event) {

switch event {

case _ as UserLoaded:

                self
= .validated(token: token)

case _ as TokenRejected:

                self
= .expired(at: Date())
            default:

break

}

}

}

case validated(token: String) {
        var description: String {

return "Token \(token) has been validated."

}

        mutating func react
(to event: Event) {

switch event {

case _ as TokenRejected:

                self
= .expired(at: Date())

case _ as UserLoggedOut:

                self
= .
none

            default:

break

}

}

}

}

On Jan 8, 2017, at 12:22 PM, Derrick Ho via swift-evolution <swift-evolution@swift.org> wrote:

Currently we can write a helper method to aid in getting the values inside the enum associated value. Below is a fully working implementation:

enum Package {
	case box(String, Int)
	case circular(String)
	
	var associated: Associated {
		return Associated(package: self)
	}
	
	struct Associated {
		let box: (String, Int)?
		let circular: (String)?
		init(package: Package) {
			switch package {
			case .box(let b):
				box = b
				circular = nil
			case .circular(let b):
				box = nil
				circular = b
			}
		}
	}
}

let b = Package.box("square", 5)
b.associated.box?.0 // Optional("square")
b.associated.box?.1 // Optional(5)
b.associated.circular // nil

let c = Package.circular("round")
c.associated.box?.0 // nil
c.associated.box?.1 // nil
c.associated.circular // Optional("round")

I had to wedge in a special type called "Associated" and had to write some boiler-plate code to get this effect. It is quite predictable and can probably be done under the hood. I would of course prefer syntactic sugar to simplify it and turn

b.associated.box?.0

into

b.box?.0

On Sun, Jan 8, 2017 at 1:05 PM David Sweeris via swift-evolution <swift-evolution@swift.org> wrote:

On Jan 8, 2017, at 06:53, Karim Nassar via swift-evolution <swift-evolution@swift.org> wrote:

One area of enums that I’d love to see some sugar wrapped around (and perhaps this has already been discussed previously?) is extracting associated values.

There are many times where, given an enum like:

enum Feedback {
  case ok
  case info(String)
  case warning(String, Location)
  case error(String, Location)
}

I’d love it if we could tag the associated values with some semantic accessor, perhaps borrowed from tuples:

enum Feedback {
  case ok
  case info(msg: String)
  case warning(msg: String, loc: Location)
  case error(msg: String, loc: Location)
}

then:

let foo = self.getSomeFeedback() // -> Feedback
if let msg = foo.msg { // since not all cases can hold a ‘msg’ .msg is an Optional
  print(foo)
}

Can't remember if it's come up before, but +1. I can't count how many times I've written something like:
enum Foo : CustomStringConvertible {
    case c1(T1)
    case c2(T2)
    ...
    case cN(TN)

    var description: String {
        switch self {
            case .c1(let val): return "\(val)"
            case .c2(let val): return "\(val)"
            ...
            case .cN(let val): return "\(val)"
        }
    }
}

Being able to simplify that to:
var description: String {
    let nilDesc = "some appropriate description"
    return "\(self.0 ?? nilDesc)"
}

Would be great.

- Dave Sweeris
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Guillaume Lessard) #7

The problem I see with this is that enums and their functions inherently multiply each other. If I have 3 cases and 3 functions or properties, there are 9 implementation details, no matter how they're organized. There can be 3 functions/properties, each with a 3-case switch, or there can be 3 enum cases each with 3 strange, partial functions/properties.

I can see why someone might prefer one over the other, but is either way truly better? The current way this works at least has the merit of not requiring a special dialect for enums.

Cheers,
Guillaume Lessard

···

On 9 janv. 2017, at 10:54, Tim Shadel via swift-evolution <swift-evolution@swift.org> wrote:

Enums get large, and they get complicated because the code for each case gets sliced up and scattered across many functions. It becomes a "one of these things is not like the other" situation because writing functions inside enums is unlike writing functions in any other part of Swift code.


(Charles Srstka) #8

A thousand +1s from me on this. This would make enums so much more pleasant to work with.

Charles

···

On Jan 9, 2017, at 11:54 AM, Tim Shadel via swift-evolution <swift-evolution@swift.org> wrote:

enum OneOnOneField: Int {

    var title: String { return "" }
    var placeholder: String { return "" }
    var image: UIImage { return UIImage() }

    case agenda {
        var title: String { return NSLocalizedString("Agenda", comment: "One on one field header") }
        var placeholder: String { return NSLocalizedString("Add an agenda", comment: "One on one field placeholder") }
        var image: UIImage { return #imageLiteral(resourceName: "Agenda-Small") }
    }

    case summary {
        var title: String { return NSLocalizedString("Summary", comment: "One on one field header") }
        var placeholder: String { return NSLocalizedString("Add a summary", comment: "One on one field placeholder") }
        var image: UIImage { return #imageLiteral(resourceName: "Summary-Small") }
    }

    case date

    case notes {
        var title: String { return NSLocalizedString("Personal Notes", comment: "Title for personal notes screen") }
    }

}


(Tim Shadel) #9

While my main focus is on the proposal about consolidating cases, I had notes about almost that exact syntax... :smiley:

enum TokenState: State {

    let id: Int
    let token: String
    
    case loading {
        let startedAt: Date

        var description: String {
            return "Loading token from disk"
        }
    }

    case none {
        let reason: String
    }

}

The main thing to note is the difference between stored properties (well, associated values) and calculated ones. I think this new syntax can be done entirely within enum's current capabilities. The `let` statements outside the case simply define some associated values used by all cases. The `let` statements inside the cases declare additional associated values used for only that case.

In all areas, anything that's a `var` is dynamic, and so repeats type information in all `case` blocks because that seems like the least disruptive way to do it.

To Guillaume's point, this actually makes enums have _less_ special syntax, and treat them more like a closed family of structs with special powers.

BUT, again my focus is first on consolidating the case logic.

—Tim

···

On Jan 9, 2017, at 11:11 AM, Sean Heber <sean@fifthace.com> wrote:

I would like something along these lines. I would suggest going farther and do something like this if possible to avoid repeating the type information all over the place:

enum OneOnOneField: Int {
  let title: String
  let placeholder: String
  let image: UIImage

  case agenda {
         title: NSLocalizedString("Agenda", comment: "One on one field header")
          placeholder: NSLocalizedString("Add an agenda", comment: "One on one field placeholder”)
    image: #imageLiteral(resourceName: "Agenda-Small”)
  }

  // etc
}

l8r
Sean

On Jan 9, 2017, at 11:54 AM, Tim Shadel via swift-evolution <swift-evolution@swift.org> wrote:

It seems like my original (simplified) examples didn't clearly highlight what the problem I'm focused on solving.

Enums get large, and they get complicated because the code for each case gets sliced up and scattered across many functions. It becomes a "one of these things is not like the other" situation because writing functions inside enums is unlike writing functions in any other part of Swift code.

Also, to be clear, my goal is _not_ code brevity. It is coherence, the property where related code is located together. Some increase in code verbosity is acceptable to make code more coherent, since that leads to long-term maintainability.

I've pulled out a few of the larger enums I've seen in code to try to illustrate this. Along the way, I've made a few alterations based on the comments I've seen come through. (Rien & Robert: I've pushed the definitions inside the declaration, to ensure they don't end up in other files since that was never my intention; Rien & Daniel: I've altered the syntax to open a brace right after the case declaration making everything outside it the default, and I like that better).

Here's an enum used to consolidate the descriptions of UI elements for a screen, allowing the datasource to alter their order, presentation, and visibility while keeping the set of possible fields clean and finite. These enum values are used much like singletons.

enum OneOnOneField: Int {

case agenda
   case summary
   case date
   case notes

   struct Info {

       var title
: String

       var placeholder
: String

       var image
:
UIImage

}

   var info
: Info {

switch self {

case .agenda:

return Info(

               title
: NSLocalizedString("Agenda", comment: "One on one field header"),

               placeholder
: NSLocalizedString("Add an agenda", comment: "One on one field placeholder"),

               image
: #imageLiteral(resourceName: "Agenda-Small"))

case .summary:

return Info(

               title
: NSLocalizedString("Summary", comment: "One on one field header"),

               placeholder
: NSLocalizedString("Add a summary", comment: "One on one field placeholder"),

               image
: #imageLiteral(resourceName: "Summary-Small"))

case .date:

return Info(title: "", placeholder: "", image: UIImage())

case .notes:

return Info(title: NSLocalizedString("Personal Notes", comment: "Title for personal notes screen"), placeholder: "", image: UIImage())

}

}
}
Consolidating them could instead look something like this:

enum OneOnOneField: Int {

   var title
: String { return "" }

   var placeholder
: String { return "" }

   var image
: UIImage { return UIImage() }

case agenda {
       var title: String { return NSLocalizedString("Agenda", comment: "One on one field header") }

       var placeholder
: String { return NSLocalizedString("Add an agenda", comment: "One on one field placeholder") }

       var image
: UIImage { return #imageLiteral(resourceName: "Agenda-Small") }

}

case summary {
       var title: String { return NSLocalizedString("Summary", comment: "One on one field header") }

       var placeholder
: String { return NSLocalizedString("Add a summary", comment: "One on one field placeholder") }

       var image
: UIImage { return #imageLiteral(resourceName: "Summary-Small") }

}

case date

   case notes {
       var title: String { return NSLocalizedString("Personal Notes", comment: "Title for personal notes screen") }

}

}
Here's an enum that implements the basics of a state machine for OAuth 2 token use, refreshing, and login. Some of its cases have associated values and some don't.

enum TokenState: State {

case loading
   case none
   case expired(Date)
   case untested(token: String)
   case validated(token: String)

   var description: String {

switch self {

case .loading:

return "Loading token from disk"

case .none:

return "No token found"

case let .expired(at):

return "Expired at \(at)"

case let .untested(token):

return "Received token \(token), but it hasn't been tested."

case let .validated(token):

return "Token \(token) has been validated."

}

}

   mutating func react
(to event: Event) {

switch self {

case .loading:

switch event {

case _ as TokenNotFound:

               self
= .
none

case let expired as TokenExpired:

               self
= .expired(expired.at)

case let loaded as TokenLoaded:

               self
= .untested(token: loaded.token)
           default:

break

}

case .none:

switch event {

case let loggedIn as UserLoggedIn:

               self
= .untested(token: loggedIn.token)
           default:

break

}

case .expired:

switch event {

case let refreshed as TokenRefreshed:

               self
= .untested(token: refreshed.token)

case _ as TokenRefreshErrored:

               self
= .
none

           default:

break

}

case let .untested(token):

switch event {

case _ as UserLoaded:

               self
= .validated(token: token)

case _ as TokenRejected:

               self
= .expired(at: Date())
           default:

break

}

case .validated:

switch event {

case _ as TokenRejected:

               self
= .expired(Date())

case _ as UserLoggedOut:

               self
= .
none

           default:

break

}

}

}

static var initialState: TokenState {

return .
loading

}

}
After consolidation, this becomes:

enum TokenState: State {

static var initialState: TokenState {

return .
loading

}

case loading {
       var description: String {

return "Loading token from disk"

}

       mutating func react
(to event: Event) {

switch event {

case _ as TokenNotFound:

               self
= .
none

case let expired as TokenExpired:

               self
= .expired(at: expired.at)

case let loaded as TokenLoaded:

               self
= .untested(token: loaded.token)
           default:

break

}

}

}

case none {
       var description: String {

return "No token found"

}

       mutating func react
(to event: Event) {

switch event {

case let loggedIn as UserLoggedIn:

               self
= .untested(token: loggedIn.token)
           default:

break

}

}

}

case expired(at: Date) {
       var description: String {

return "Expired at \(at)"

}

       mutating func react
(to event: Event) {

switch event {

case let refreshed as TokenRefreshed:

               self
= .untested(token: refreshed.token)

case _ as TokenRefreshErrored:

               self
= .
none

           default:

break

}

}

}

case untested(token: String) {
       var description: String {

return "Received token \(token), but it hasn't been tested."

}

       mutating func react
(to event: Event) {

switch event {

case _ as UserLoaded:

               self
= .validated(token: token)

case _ as TokenRejected:

               self
= .expired(at: Date())
           default:

break

}

}

}

case validated(token: String) {
       var description: String {

return "Token \(token) has been validated."

}

       mutating func react
(to event: Event) {

switch event {

case _ as TokenRejected:

               self
= .expired(at: Date())

case _ as UserLoggedOut:

               self
= .
none

           default:

break

}

}

}

}

On Jan 8, 2017, at 12:22 PM, Derrick Ho via swift-evolution <swift-evolution@swift.org> wrote:

Currently we can write a helper method to aid in getting the values inside the enum associated value. Below is a fully working implementation:

enum Package {
	case box(String, Int)
	case circular(String)
	
	var associated: Associated {
		return Associated(package: self)
	}
	
	struct Associated {
		let box: (String, Int)?
		let circular: (String)?
		init(package: Package) {
			switch package {
			case .box(let b):
				box = b
				circular = nil
			case .circular(let b):
				box = nil
				circular = b
			}
		}
	}
}

let b = Package.box("square", 5)
b.associated.box?.0 // Optional("square")
b.associated.box?.1 // Optional(5)
b.associated.circular // nil

let c = Package.circular("round")
c.associated.box?.0 // nil
c.associated.box?.1 // nil
c.associated.circular // Optional("round")

I had to wedge in a special type called "Associated" and had to write some boiler-plate code to get this effect. It is quite predictable and can probably be done under the hood. I would of course prefer syntactic sugar to simplify it and turn

b.associated.box?.0

into

b.box?.0

On Sun, Jan 8, 2017 at 1:05 PM David Sweeris via swift-evolution <swift-evolution@swift.org> wrote:

On Jan 8, 2017, at 06:53, Karim Nassar via swift-evolution <swift-evolution@swift.org> wrote:

One area of enums that I’d love to see some sugar wrapped around (and perhaps this has already been discussed previously?) is extracting associated values.

There are many times where, given an enum like:

enum Feedback {
  case ok
  case info(String)
  case warning(String, Location)
  case error(String, Location)
}

I’d love it if we could tag the associated values with some semantic accessor, perhaps borrowed from tuples:

enum Feedback {
  case ok
  case info(msg: String)
  case warning(msg: String, loc: Location)
  case error(msg: String, loc: Location)
}

then:

let foo = self.getSomeFeedback() // -> Feedback
if let msg = foo.msg { // since not all cases can hold a ‘msg’ .msg is an Optional
  print(foo)
}

Can't remember if it's come up before, but +1. I can't count how many times I've written something like:
enum Foo : CustomStringConvertible {
   case c1(T1)
   case c2(T2)
   ...
   case cN(TN)

   var description: String {
       switch self {
           case .c1(let val): return "\(val)"
           case .c2(let val): return "\(val)"
           ...
           case .cN(let val): return "\(val)"
       }
   }
}

Being able to simplify that to:
var description: String {
   let nilDesc = "some appropriate description"
   return "\(self.0 ?? nilDesc)"
}

Would be great.

- Dave Sweeris
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Sean Heber) #10

I’m not sure how to argue this, but I feel pretty strongly that something more like this proposed organization *is* actually better. That said, I do not think this conflicts with the current design of enums, however, so this is likely purely additive. The current design makes some situations almost comically verbose and disorganized, IMO, but it *is* right for other situations. We may want to have both.

l8r
Sean

···

On Jan 9, 2017, at 2:28 PM, Guillaume Lessard via swift-evolution <swift-evolution@swift.org> wrote:

On 9 janv. 2017, at 10:54, Tim Shadel via swift-evolution <swift-evolution@swift.org> wrote:

Enums get large, and they get complicated because the code for each case gets sliced up and scattered across many functions. It becomes a "one of these things is not like the other" situation because writing functions inside enums is unlike writing functions in any other part of Swift code.

The problem I see with this is that enums and their functions inherently multiply each other. If I have 3 cases and 3 functions or properties, there are 9 implementation details, no matter how they're organized. There can be 3 functions/properties, each with a 3-case switch, or there can be 3 enum cases each with 3 strange, partial functions/properties.

I can see why someone might prefer one over the other, but is either way truly better? The current way this works at least has the merit of not requiring a special dialect for enums.


(Tony Allevato) #11

While I do like the consolidated syntax more than most of the alternatives
I've seen to address this problem, any proposed solution also needs to
address how it would work with cases that have associated values. That
complicates the syntax somewhat.

···

On Mon, Jan 9, 2017 at 12:37 PM Sean Heber via swift-evolution < swift-evolution@swift.org> wrote:

> On Jan 9, 2017, at 2:28 PM, Guillaume Lessard via swift-evolution < > swift-evolution@swift.org> wrote:
>
>
>> On 9 janv. 2017, at 10:54, Tim Shadel via swift-evolution < > swift-evolution@swift.org> wrote:
>>
>> Enums get large, and they get complicated because the code for each
case gets sliced up and scattered across many functions. It becomes a "one
of these things is not like the other" situation because writing functions
inside enums is unlike writing functions in any other part of Swift code.
>
> The problem I see with this is that enums and their functions inherently
multiply each other. If I have 3 cases and 3 functions or properties, there
are 9 implementation details, no matter how they're organized. There can be
3 functions/properties, each with a 3-case switch, or there can be 3 enum
cases each with 3 strange, partial functions/properties.
>
> I can see why someone might prefer one over the other, but is either way
truly better? The current way this works at least has the merit of not
requiring a special dialect for enums.

I’m not sure how to argue this, but I feel pretty strongly that something
more like this proposed organization *is* actually better. That said, I do
not think this conflicts with the current design of enums, however, so this
is likely purely additive. The current design makes some situations almost
comically verbose and disorganized, IMO, but it *is* right for other
situations. We may want to have both.

l8r
Sean
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Tim Shadel) #12

There are examples of associated values in the proposed syntax. Which parts should I provide more detail on?

···

On Jan 9, 2017, at 1:43 PM, Tony Allevato via swift-evolution <swift-evolution@swift.org> wrote:

While I do like the consolidated syntax more than most of the alternatives I've seen to address this problem, any proposed solution also needs to address how it would work with cases that have associated values. That complicates the syntax somewhat.

On Mon, Jan 9, 2017 at 12:37 PM Sean Heber via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

> On Jan 9, 2017, at 2:28 PM, Guillaume Lessard via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>
>
>> On 9 janv. 2017, at 10:54, Tim Shadel via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>>
>> Enums get large, and they get complicated because the code for each case gets sliced up and scattered across many functions. It becomes a "one of these things is not like the other" situation because writing functions inside enums is unlike writing functions in any other part of Swift code.
>
> The problem I see with this is that enums and their functions inherently multiply each other. If I have 3 cases and 3 functions or properties, there are 9 implementation details, no matter how they're organized. There can be 3 functions/properties, each with a 3-case switch, or there can be 3 enum cases each with 3 strange, partial functions/properties.
>
> I can see why someone might prefer one over the other, but is either way truly better? The current way this works at least has the merit of not requiring a special dialect for enums.

I’m not sure how to argue this, but I feel pretty strongly that something more like this proposed organization *is* actually better. That said, I do not think this conflicts with the current design of enums, however, so this is likely purely additive. The current design makes some situations almost comically verbose and disorganized, IMO, but it *is* right for other situations. We may want to have both.

l8r
Sean
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Tony Allevato) #13

Ah, my apologies—the syntax highlighting in the thread was throwing off my
e-mail client and I was having trouble reading it.

Associated values don't necessarily have to have names: I can write "case
.foo(Int)". Since your examples use the associated value label as the name
of the value inside the body, how would you handle those label-less values?

···

On Mon, Jan 9, 2017 at 1:06 PM Tim Shadel <timshadel@gmail.com> wrote:

There are examples of associated values in the proposed syntax. Which
parts should I provide more detail on?

On Jan 9, 2017, at 1:43 PM, Tony Allevato via swift-evolution < > swift-evolution@swift.org> wrote:

While I do like the consolidated syntax more than most of the alternatives
I've seen to address this problem, any proposed solution also needs to
address how it would work with cases that have associated values. That
complicates the syntax somewhat.

On Mon, Jan 9, 2017 at 12:37 PM Sean Heber via swift-evolution < > swift-evolution@swift.org> wrote:

> On Jan 9, 2017, at 2:28 PM, Guillaume Lessard via swift-evolution < > swift-evolution@swift.org> wrote:
>
>
>> On 9 janv. 2017, at 10:54, Tim Shadel via swift-evolution < > swift-evolution@swift.org> wrote:
>>
>> Enums get large, and they get complicated because the code for each
case gets sliced up and scattered across many functions. It becomes a "one
of these things is not like the other" situation because writing functions
inside enums is unlike writing functions in any other part of Swift code.
>
> The problem I see with this is that enums and their functions inherently
multiply each other. If I have 3 cases and 3 functions or properties, there
are 9 implementation details, no matter how they're organized. There can be
3 functions/properties, each with a 3-case switch, or there can be 3 enum
cases each with 3 strange, partial functions/properties.
>
> I can see why someone might prefer one over the other, but is either way
truly better? The current way this works at least has the merit of not
requiring a special dialect for enums.

I’m not sure how to argue this, but I feel pretty strongly that something
more like this proposed organization *is* actually better. That said, I do
not think this conflicts with the current design of enums, however, so this
is likely purely additive. The current design makes some situations almost
comically verbose and disorganized, IMO, but it *is* right for other
situations. We may want to have both.

l8r
Sean
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Tim Shadel) #14

I can see it two different ways: 1) this is optional syntax, and doesn't replace or deprecate any existing syntax, so if you want to use it you have to have a label for the associated value, or 2) you may omit the label if you never reference the associated value within any of the calculated fields inside the case block.

That's perhaps a bit harsh, so I'm open to other ideas. I'm mostly concerned with keeping the proposal simple enough to be a "hidden switch" statement so that it doesn't grow too large or complicated, but I recognize that perhaps I'm erring more on the conservative side than necessary here. :slight_smile:

—Tim

···

On Jan 9, 2017, at 2:11 PM, Tony Allevato <tony.allevato@gmail.com> wrote:

Ah, my apologies—the syntax highlighting in the thread was throwing off my e-mail client and I was having trouble reading it.

Associated values don't necessarily have to have names: I can write "case .foo(Int)". Since your examples use the associated value label as the name of the value inside the body, how would you handle those label-less values?

On Mon, Jan 9, 2017 at 1:06 PM Tim Shadel <timshadel@gmail.com <mailto:timshadel@gmail.com>> wrote:
There are examples of associated values in the proposed syntax. Which parts should I provide more detail on?

On Jan 9, 2017, at 1:43 PM, Tony Allevato via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

While I do like the consolidated syntax more than most of the alternatives I've seen to address this problem, any proposed solution also needs to address how it would work with cases that have associated values. That complicates the syntax somewhat.

On Mon, Jan 9, 2017 at 12:37 PM Sean Heber via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

> On Jan 9, 2017, at 2:28 PM, Guillaume Lessard via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>
>
>> On 9 janv. 2017, at 10:54, Tim Shadel via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>>
>> Enums get large, and they get complicated because the code for each case gets sliced up and scattered across many functions. It becomes a "one of these things is not like the other" situation because writing functions inside enums is unlike writing functions in any other part of Swift code.
>
> The problem I see with this is that enums and their functions inherently multiply each other. If I have 3 cases and 3 functions or properties, there are 9 implementation details, no matter how they're organized. There can be 3 functions/properties, each with a 3-case switch, or there can be 3 enum cases each with 3 strange, partial functions/properties.
>
> I can see why someone might prefer one over the other, but is either way truly better? The current way this works at least has the merit of not requiring a special dialect for enums.

I’m not sure how to argue this, but I feel pretty strongly that something more like this proposed organization *is* actually better. That said, I do not think this conflicts with the current design of enums, however, so this is likely purely additive. The current design makes some situations almost comically verbose and disorganized, IMO, but it *is* right for other situations. We may want to have both.

l8r
Sean
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Tim Shadel) #15

Yeah, that's much nicer than what I just sent! :smiley:

···

On Jan 9, 2017, at 2:16 PM, Sean Heber <sean@fifthace.com> wrote:

I can’t speak for Tim, but I’d suggest just unifying the case syntax with functions so they become:

case foo(_ thing: Int)

And if you don’t actually need to ever *use* it by name in your enum properties/functions (if you even have any), then you could leave it out and write it like it is now, but that’d become “sugar”:

case foo(Int)

l8r
Sean

On Jan 9, 2017, at 3:11 PM, Tony Allevato <tony.allevato@gmail.com> wrote:

Ah, my apologies—the syntax highlighting in the thread was throwing off my e-mail client and I was having trouble reading it.

Associated values don't necessarily have to have names: I can write "case .foo(Int)". Since your examples use the associated value label as the name of the value inside the body, how would you handle those label-less values?

On Mon, Jan 9, 2017 at 1:06 PM Tim Shadel <timshadel@gmail.com> wrote:
There are examples of associated values in the proposed syntax. Which parts should I provide more detail on?

On Jan 9, 2017, at 1:43 PM, Tony Allevato via swift-evolution <swift-evolution@swift.org> wrote:

While I do like the consolidated syntax more than most of the alternatives I've seen to address this problem, any proposed solution also needs to address how it would work with cases that have associated values. That complicates the syntax somewhat.

On Mon, Jan 9, 2017 at 12:37 PM Sean Heber via swift-evolution <swift-evolution@swift.org> wrote:

On Jan 9, 2017, at 2:28 PM, Guillaume Lessard via swift-evolution <swift-evolution@swift.org> wrote:

On 9 janv. 2017, at 10:54, Tim Shadel via swift-evolution <swift-evolution@swift.org> wrote:

Enums get large, and they get complicated because the code for each case gets sliced up and scattered across many functions. It becomes a "one of these things is not like the other" situation because writing functions inside enums is unlike writing functions in any other part of Swift code.

The problem I see with this is that enums and their functions inherently multiply each other. If I have 3 cases and 3 functions or properties, there are 9 implementation details, no matter how they're organized. There can be 3 functions/properties, each with a 3-case switch, or there can be 3 enum cases each with 3 strange, partial functions/properties.

I can see why someone might prefer one over the other, but is either way truly better? The current way this works at least has the merit of not requiring a special dialect for enums.

I’m not sure how to argue this, but I feel pretty strongly that something more like this proposed organization *is* actually better. That said, I do not think this conflicts with the current design of enums, however, so this is likely purely additive. The current design makes some situations almost comically verbose and disorganized, IMO, but it *is* right for other situations. We may want to have both.

l8r
Sean
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Sean Heber) #16

I can’t speak for Tim, but I’d suggest just unifying the case syntax with functions so they become:

case foo(_ thing: Int)

And if you don’t actually need to ever *use* it by name in your enum properties/functions (if you even have any), then you could leave it out and write it like it is now, but that’d become “sugar”:

case foo(Int)

l8r
Sean

···

On Jan 9, 2017, at 3:11 PM, Tony Allevato <tony.allevato@gmail.com> wrote:

Ah, my apologies—the syntax highlighting in the thread was throwing off my e-mail client and I was having trouble reading it.

Associated values don't necessarily have to have names: I can write "case .foo(Int)". Since your examples use the associated value label as the name of the value inside the body, how would you handle those label-less values?

On Mon, Jan 9, 2017 at 1:06 PM Tim Shadel <timshadel@gmail.com> wrote:
There are examples of associated values in the proposed syntax. Which parts should I provide more detail on?

On Jan 9, 2017, at 1:43 PM, Tony Allevato via swift-evolution <swift-evolution@swift.org> wrote:

While I do like the consolidated syntax more than most of the alternatives I've seen to address this problem, any proposed solution also needs to address how it would work with cases that have associated values. That complicates the syntax somewhat.

On Mon, Jan 9, 2017 at 12:37 PM Sean Heber via swift-evolution <swift-evolution@swift.org> wrote:

> On Jan 9, 2017, at 2:28 PM, Guillaume Lessard via swift-evolution <swift-evolution@swift.org> wrote:
>
>
>> On 9 janv. 2017, at 10:54, Tim Shadel via swift-evolution <swift-evolution@swift.org> wrote:
>>
>> Enums get large, and they get complicated because the code for each case gets sliced up and scattered across many functions. It becomes a "one of these things is not like the other" situation because writing functions inside enums is unlike writing functions in any other part of Swift code.
>
> The problem I see with this is that enums and their functions inherently multiply each other. If I have 3 cases and 3 functions or properties, there are 9 implementation details, no matter how they're organized. There can be 3 functions/properties, each with a 3-case switch, or there can be 3 enum cases each with 3 strange, partial functions/properties.
>
> I can see why someone might prefer one over the other, but is either way truly better? The current way this works at least has the merit of not requiring a special dialect for enums.

I’m not sure how to argue this, but I feel pretty strongly that something more like this proposed organization *is* actually better. That said, I do not think this conflicts with the current design of enums, however, so this is likely purely additive. The current design makes some situations almost comically verbose and disorganized, IMO, but it *is* right for other situations. We may want to have both.

l8r
Sean
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Tony Allevato) #17

I like that approach a lot (and it would be nice to use separate labels vs.
argument names in the case where they do have labels, too).

Enum cases with associated values are really just sugar for static methods
on the enum type *anyway* with the added pattern matching abilities, so
unifying the syntax seems like a positive direction to go in.

···

On Mon, Jan 9, 2017 at 1:20 PM Tim Shadel <timshadel@gmail.com> wrote:

Yeah, that's much nicer than what I just sent! :smiley:

> On Jan 9, 2017, at 2:16 PM, Sean Heber <sean@fifthace.com> wrote:
>
> I can’t speak for Tim, but I’d suggest just unifying the case syntax
with functions so they become:
>
> case foo(_ thing: Int)
>
> And if you don’t actually need to ever *use* it by name in your enum
properties/functions (if you even have any), then you could leave it out
and write it like it is now, but that’d become “sugar”:
>
> case foo(Int)
>
> l8r
> Sean
>
>
>> On Jan 9, 2017, at 3:11 PM, Tony Allevato <tony.allevato@gmail.com> > wrote:
>>
>> Ah, my apologies—the syntax highlighting in the thread was throwing off
my e-mail client and I was having trouble reading it.
>>
>> Associated values don't necessarily have to have names: I can write
"case .foo(Int)". Since your examples use the associated value label as the
name of the value inside the body, how would you handle those label-less
values?
>>
>>
>> On Mon, Jan 9, 2017 at 1:06 PM Tim Shadel <timshadel@gmail.com> wrote:
>> There are examples of associated values in the proposed syntax. Which
parts should I provide more detail on?
>>
>>> On Jan 9, 2017, at 1:43 PM, Tony Allevato via swift-evolution < > swift-evolution@swift.org> wrote:
>>>
>>> While I do like the consolidated syntax more than most of the
alternatives I've seen to address this problem, any proposed solution also
needs to address how it would work with cases that have associated values.
That complicates the syntax somewhat.
>>>
>>>
>>> On Mon, Jan 9, 2017 at 12:37 PM Sean Heber via swift-evolution < > swift-evolution@swift.org> wrote:
>>>
>>>> On Jan 9, 2017, at 2:28 PM, Guillaume Lessard via swift-evolution < > swift-evolution@swift.org> wrote:
>>>>
>>>>
>>>>> On 9 janv. 2017, at 10:54, Tim Shadel via swift-evolution < > swift-evolution@swift.org> wrote:
>>>>>
>>>>> Enums get large, and they get complicated because the code for each
case gets sliced up and scattered across many functions. It becomes a "one
of these things is not like the other" situation because writing functions
inside enums is unlike writing functions in any other part of Swift code.
>>>>
>>>> The problem I see with this is that enums and their functions
inherently multiply each other. If I have 3 cases and 3 functions or
properties, there are 9 implementation details, no matter how they're
organized. There can be 3 functions/properties, each with a 3-case switch,
or there can be 3 enum cases each with 3 strange, partial
functions/properties.
>>>>
>>>> I can see why someone might prefer one over the other, but is either
way truly better? The current way this works at least has the merit of not
requiring a special dialect for enums.
>>>
>>> I’m not sure how to argue this, but I feel pretty strongly that
something more like this proposed organization *is* actually better. That
said, I do not think this conflicts with the current design of enums,
however, so this is likely purely additive. The current design makes some
situations almost comically verbose and disorganized, IMO, but it *is*
right for other situations. We may want to have both.
>>>
>>> l8r
>>> Sean
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution@swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution@swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>
>


(Tim Shadel) #18

OK. I've taken the most recent changes from this thread and put together a draft for a proposal.

https://gist.github.com/timshadel/5a5a8e085a6fd591483a933e603c2562

I'd appreciate your review, especially to ensure I've covered all the important scenarios. I've taken the 3 associated value scenarios (none, unlabeled, labeled) and shown them in each example (calculated value, func, default, error). I've included the raw text below, without any syntax highlighting.

My big question is: does the error case in the last example affect ABI requirements, in order to display the error at the correct case line? I assume it doesn't, but that's an area I don't know well.

Thanks!

Tim

···

===============

# Enum Case Blocks

* Proposal: SE-XXXX
* Authors: [Tim Shadel](https://github.com/timshadel)
* Review Manager: TBD
* Status: **TBD**

## Motivation

Add an optional syntax to declare all code related to a single `case` in one spot. For complex `enum`s, this makes it easier to ensure that all the pieces mesh coherently across that one case, and to review all logic associated with a single `case`. This syntax is frequently more verbose in order to achieve a more coherent code structure, so its use will be most valuable in complex enums.

Swift-evolution thread: [Consolidate Code for Each Case in Enum](https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170102/029966.html)

## Proposed solution

Allow an optional block directly after the `case` declaration on an `enum`. Construct a hidden `switch self` statement for each calculated value or `func` defined in any case block. Use the body of each such calculated value in the hidden `switch self` under the appropriate case. Because switch statements must be exhaustive, the calculated value or `func` must be defined in each case block or have a `default` value to avoid an error. Defining the `func` or calculated value outside a case block defines the default case for the `switch self`. To reference an associated value within any of the items in a case block requires the value be labeled, or use a new syntax `case(_ label: Type)` to provide a local-only name for the associated value.

## Examples

All examples below are evolutions of this simple enum.

enum AuthenticationState {
    case invalid
    case expired(Date)
    case validated(token: String)
}

### Basic example

First, let's add `CustomStringConvertible` conformance to our enum.

enum AuthenticationState: CustomStringConvertible {

    case invalid {
        var description: String { return "Authentication invalid." }
    }

    case expired(_ expiration: Date) {
        var description: String { return "Authentication expired at \(expiration)." }
    }

    case validated(token: String) {
        var description: String { return "The authentication token is \(token)." }
    }

}

This is identical to the following snippet of Swift 3 code:

enum AuthenticationState: CustomStringConvertible {

    case invalid
    case expired(Date)
    case validated(token: String)

    var description: String {
        switch self {
        case invalid:
            return "Authentication invalid."
        case let expired(expiration):
            return "Authentication expired at \(expiration)."
        case let validated(token):
            return "The authentication token is \(token)."
        }
    }

}

### Extended example

Now let's have our enum conform to this simple `State` protocol, which expects each state to be able to update itself in reaction to an `Event`. This example begins to show how this optional syntax give better coherence to the enum code by placing code related to a single case in a single enclosure.

protocol State {
    mutating func react(to event: Event)
}

enum AuthenticationState: State, CustomStringConvertible {

    case invalid {
        var description: String { return "Authentication invalid." }

        mutating func react(to event: Event) {
            switch event {
            case let login as UserLoggedIn:
                self = .validated(token: login.token)
            default:
                break
            }
        }
    }

    case expired(_ expiration: Date) {
        var description: String { return "Authentication expired at \(expiration)." }

        mutating func react(to event: Event) {
            switch event {
            case let refreshed as TokenRefreshed:
                self = .validated(token: refreshed.token)
            default:
                break
            }
        }
    }

    case validated(token: String) {
        var description: String { return "The authentication token is \(token)." }

        mutating func react(to event: Event) {
            switch event {
            case let expiration as TokenExpired:
                print("Expiring token: \(token)")
                self = .expired(expiration.date)
            case _ as TokenRejected:
                self = .invalid
            case _ as UserLoggedOut:
                self = .invalid
            default:
                break
            }
        }
    }

}

This becomes identical to the following Swift 3 code:

enum AuthenticationState: State, CustomStringConvertible {

    case invalid
    case expired(Date)
    case validated(token: String)

    var description: String {
        switch self {
        case invalid:
            return "Authentication invalid."
        case let expired(expiration):
            return "Authentication expired at \(expiration)."
        case let validated(token):
            return "The authentication token is \(token)."
        }
    }

    mutating func react(to event: Event) {
        switch self {
        case invalid: {
            switch event {
            case let login as UserLoggedIn:
                self = .validated(token: login.token)
            default:
                break
            }
        }
        case let expired(expiration) {
            switch event {
            case let refreshed as TokenRefreshed:
                self = .validated(token: refreshed.token)
            default:
                break
            }
        }
        case let validated(token) {
            switch event {
            case let expiration as TokenExpired:
                print("Expiring token: \(token)")
                self = .expired(expiration.date)
            case _ as TokenRejected:
                self = .invalid
            case _ as UserLoggedOut:
                self = .invalid
            default:
                break
            }
        }
    }

}

### Default case example

Let's go back to the simple example to demonstrate declaring a default case.

enum AuthenticationState: CustomStringConvertible {

    var description: String { return "" }

    case invalid
    case expired(Date)
    case validated(token: String) {
        var description: String { return "The authentication token is \(token)." }
    }

}

Is identical to this Swift 3 code:

enum AuthenticationState: CustomStringConvertible {

    case invalid
    case expired(Date)
    case validated(token: String)

    var description: String {
        switch self {
        case let validated(token):
            return "The authentication token is \(token)."
        default:
            return ""
        }
    }

}

### Error example

Finally, here's what happens when a case fails to add a block when no default is defined.

enum AuthenticationState: CustomStringConvertible {

    case invalid  <<< error: description must be exhaustively defined. Missing block for case .invalid.

    case expired(Date)  <<< error: description must be exhaustively defined. Missing block for case .expired.

    case validated(token: String) {
        var description: String { return "The authentication token is \(token)." }
    }

}

## Source compatibility

No source is deprecated in this proposal, so source compatibility should be preserved.

## Effect on ABI stability

Because the generated switch statement should be identical to one that can be generated with Swift 3, I don't foresee effect on ABI stability.

Question: does the error case above affect ABI requirements, in order to display the error at the correct case line?

## Alternatives considered

Use of the `extension` keyword was discussed and quickly rejected for numerous reasons.

On Jan 9, 2017, at 2:22 PM, Tony Allevato <tony.allevato@gmail.com> wrote:

I like that approach a lot (and it would be nice to use separate labels vs. argument names in the case where they do have labels, too).

Enum cases with associated values are really just sugar for static methods on the enum type *anyway* with the added pattern matching abilities, so unifying the syntax seems like a positive direction to go in.

On Mon, Jan 9, 2017 at 1:20 PM Tim Shadel <timshadel@gmail.com <mailto:timshadel@gmail.com>> wrote:
Yeah, that's much nicer than what I just sent! :smiley:

> On Jan 9, 2017, at 2:16 PM, Sean Heber <sean@fifthace.com <mailto:sean@fifthace.com>> wrote:
>
> I can’t speak for Tim, but I’d suggest just unifying the case syntax with functions so they become:
>
> case foo(_ thing: Int)
>
> And if you don’t actually need to ever *use* it by name in your enum properties/functions (if you even have any), then you could leave it out and write it like it is now, but that’d become “sugar”:
>
> case foo(Int)
>
> l8r
> Sean
>
>
>> On Jan 9, 2017, at 3:11 PM, Tony Allevato <tony.allevato@gmail.com <mailto:tony.allevato@gmail.com>> wrote:
>>
>> Ah, my apologies—the syntax highlighting in the thread was throwing off my e-mail client and I was having trouble reading it.
>>
>> Associated values don't necessarily have to have names: I can write "case .foo(Int)". Since your examples use the associated value label as the name of the value inside the body, how would you handle those label-less values?
>>
>>
>> On Mon, Jan 9, 2017 at 1:06 PM Tim Shadel <timshadel@gmail.com <mailto:timshadel@gmail.com>> wrote:
>> There are examples of associated values in the proposed syntax. Which parts should I provide more detail on?
>>
>>> On Jan 9, 2017, at 1:43 PM, Tony Allevato via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>>>
>>> While I do like the consolidated syntax more than most of the alternatives I've seen to address this problem, any proposed solution also needs to address how it would work with cases that have associated values. That complicates the syntax somewhat.
>>>
>>>
>>> On Mon, Jan 9, 2017 at 12:37 PM Sean Heber via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>>>
>>>> On Jan 9, 2017, at 2:28 PM, Guillaume Lessard via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>>>>
>>>>
>>>>> On 9 janv. 2017, at 10:54, Tim Shadel via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>>>>>
>>>>> Enums get large, and they get complicated because the code for each case gets sliced up and scattered across many functions. It becomes a "one of these things is not like the other" situation because writing functions inside enums is unlike writing functions in any other part of Swift code.
>>>>
>>>> The problem I see with this is that enums and their functions inherently multiply each other. If I have 3 cases and 3 functions or properties, there are 9 implementation details, no matter how they're organized. There can be 3 functions/properties, each with a 3-case switch, or there can be 3 enum cases each with 3 strange, partial functions/properties.
>>>>
>>>> I can see why someone might prefer one over the other, but is either way truly better? The current way this works at least has the merit of not requiring a special dialect for enums.
>>>
>>> I’m not sure how to argue this, but I feel pretty strongly that something more like this proposed organization *is* actually better. That said, I do not think this conflicts with the current design of enums, however, so this is likely purely additive. The current design makes some situations almost comically verbose and disorganized, IMO, but it *is* right for other situations. We may want to have both.
>>>
>>> l8r
>>> Sean
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution@swift.org <mailto:swift-evolution@swift.org>
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution@swift.org <mailto:swift-evolution@swift.org>
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>
>


(Derrick Ho) #19

Interesting proposal Tim. So instead of repeating the enum cases multiple
times we repeat the functions multiple times?

I feel like this merely flipping the complexity but not really getting rid
of it.

···

On Tue, Jan 10, 2017 at 8:08 PM Tim Shadel via swift-evolution < swift-evolution@swift.org> wrote:

OK. I've taken the most recent changes from this thread and put together a
draft for a proposal.

https://gist.github.com/timshadel/5a5a8e085a6fd591483a933e603c2562

I'd appreciate your review, especially to ensure I've covered all the
important scenarios. I've taken the 3 associated value scenarios (none,
unlabeled, labeled) and shown them in each example (calculated value, func,
default, error). I've included the raw text below, without any syntax
highlighting.

My big question is: does the error case in the last example affect ABI
requirements, in order to display the error at the correct case line? I
assume it doesn't, but that's an area I don't know well.

Thanks!

Tim

===============

# Enum Case Blocks

* Proposal: SE-XXXX
* Authors: [Tim Shadel](https://github.com/timshadel)
* Review Manager: TBD
* Status: **TBD**

## Motivation

Add an optional syntax to declare all code related to a single `case` in
one spot. For complex `enum`s, this makes it easier to ensure that all the
pieces mesh coherently across that one case, and to review all logic
associated with a single `case`. This syntax is frequently more verbose in
order to achieve a more coherent code structure, so its use will be most
valuable in complex enums.

Swift-evolution thread: [Consolidate Code for Each Case in Enum](
https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20170102/029966.html
)

## Proposed solution

Allow an optional block directly after the `case` declaration on an
`enum`. Construct a hidden `switch self` statement for each calculated
value or `func` defined in any case block. Use the body of each such
calculated value in the hidden `switch self` under the appropriate case.
Because switch statements must be exhaustive, the calculated value or
`func` must be defined in each case block or have a `default` value to
avoid an error. Defining the `func` or calculated value outside a case
block defines the default case for the `switch self`. To reference an
associated value within any of the items in a case block requires the value
be labeled, or use a new syntax `case(_ label: Type)` to provide a
local-only name for the associated value.

## Examples

All examples below are evolutions of this simple enum.

enum AuthenticationState {
    case invalid
    case expired(Date)
    case validated(token: String)
}

### Basic example

First, let's add `CustomStringConvertible` conformance to our enum.

enum AuthenticationState: CustomStringConvertible {

    case invalid {
        var description: String { return "Authentication invalid." }
    }

    case expired(_ expiration: Date) {
        var description: String { return "Authentication expired at
\(expiration)." }
    }

    case validated(token: String) {
        var description: String { return "The authentication token is
\(token)." }
    }

}

This is identical to the following snippet of Swift 3 code:

enum AuthenticationState: CustomStringConvertible {

    case invalid
    case expired(Date)
    case validated(token: String)

    var description: String {
        switch self {
        case invalid:
            return "Authentication invalid."
        case let expired(expiration):
            return "Authentication expired at \(expiration)."
        case let validated(token):
            return "The authentication token is \(token)."
        }
    }

}

### Extended example

Now let's have our enum conform to this simple `State` protocol, which
expects each state to be able to update itself in reaction to an `Event`.
This example begins to show how this optional syntax give better coherence
to the enum code by placing code related to a single case in a single
enclosure.

protocol State {
    mutating func react(to event: Event)
}

enum AuthenticationState: State, CustomStringConvertible {

    case invalid {
        var description: String { return "Authentication invalid." }

        mutating func react(to event: Event) {
            switch event {
            case let login as UserLoggedIn:
                self = .validated(token: login.token)
            default:
                break
            }
        }
    }

    case expired(_ expiration: Date) {
        var description: String { return "Authentication expired at
\(expiration)." }

        mutating func react(to event: Event) {
            switch event {
            case let refreshed as TokenRefreshed:
                self = .validated(token: refreshed.token)
            default:
                break
            }
        }
    }

    case validated(token: String) {
        var description: String { return "The authentication token is
\(token)." }

        mutating func react(to event: Event) {
            switch event {
            case let expiration as TokenExpired:
                print("Expiring token: \(token)")
                self = .expired(expiration.date)
            case _ as TokenRejected:
                self = .invalid
            case _ as UserLoggedOut:
                self = .invalid
            default:
                break
            }
        }
    }

}

This becomes identical to the following Swift 3 code:

enum AuthenticationState: State, CustomStringConvertible {

    case invalid
    case expired(Date)
    case validated(token: String)

    var description: String {
        switch self {
        case invalid:
            return "Authentication invalid."
        case let expired(expiration):
            return "Authentication expired at \(expiration)."
        case let validated(token):
            return "The authentication token is \(token)."
        }
    }

    mutating func react(to event: Event) {
        switch self {
        case invalid: {
            switch event {
            case let login as UserLoggedIn:
                self = .validated(token: login.token)
            default:
                break
            }
        }
        case let expired(expiration) {
            switch event {
            case let refreshed as TokenRefreshed:
                self = .validated(token: refreshed.token)
            default:
                break
            }
        }
        case let validated(token) {
            switch event {
            case let expiration as TokenExpired:
                print("Expiring token: \(token)")
                self = .expired(expiration.date)
            case _ as TokenRejected:
                self = .invalid
            case _ as UserLoggedOut:
                self = .invalid
            default:
                break
            }
        }
    }

}

### Default case example

Let's go back to the simple example to demonstrate declaring a default
case.

enum AuthenticationState: CustomStringConvertible {

    var description: String { return "" }

    case invalid
    case expired(Date)
    case validated(token: String) {
        var description: String { return "The authentication token is
\(token)." }
    }

}

Is identical to this Swift 3 code:

enum AuthenticationState: CustomStringConvertible {

    case invalid
    case expired(Date)
    case validated(token: String)

    var description: String {
        switch self {
        case let validated(token):
            return "The authentication token is \(token)."
        default:
            return ""
        }
    }

}

### Error example

Finally, here's what happens when a case fails to add a block when no
default is defined.

enum AuthenticationState: CustomStringConvertible {

    case invalid  <<< error: description must be exhaustively defined.
Missing block for case .invalid.

    case expired(Date)  <<< error: description must be exhaustively
defined. Missing block for case .expired.

    case validated(token: String) {
        var description: String { return "The authentication token is
\(token)." }
    }

}

## Source compatibility

No source is deprecated in this proposal, so source compatibility should
be preserved.

## Effect on ABI stability

Because the generated switch statement should be identical to one that can
be generated with Swift 3, I don't foresee effect on ABI stability.

Question: does the error case above affect ABI requirements, in order to
display the error at the correct case line?

## Alternatives considered

Use of the `extension` keyword was discussed and quickly rejected for
numerous reasons.

On Jan 9, 2017, at 2:22 PM, Tony Allevato <tony.allevato@gmail.com> wrote:

I like that approach a lot (and it would be nice to use separate labels
vs. argument names in the case where they do have labels, too).

Enum cases with associated values are really just sugar for static methods
on the enum type *anyway* with the added pattern matching abilities, so
unifying the syntax seems like a positive direction to go in.

On Mon, Jan 9, 2017 at 1:20 PM Tim Shadel <timshadel@gmail.com> wrote:

Yeah, that's much nicer than what I just sent! :smiley:

> On Jan 9, 2017, at 2:16 PM, Sean Heber <sean@fifthace.com> wrote:
>
> I can’t speak for Tim, but I’d suggest just unifying the case syntax
with functions so they become:
>
> case foo(_ thing: Int)
>
> And if you don’t actually need to ever *use* it by name in your enum
properties/functions (if you even have any), then you could leave it out
and write it like it is now, but that’d become “sugar”:
>
> case foo(Int)
>
> l8r
> Sean
>
>
>> On Jan 9, 2017, at 3:11 PM, Tony Allevato <tony.allevato@gmail.com> > wrote:
>>
>> Ah, my apologies—the syntax highlighting in the thread was throwing off
my e-mail client and I was having trouble reading it.
>>
>> Associated values don't necessarily have to have names: I can write
"case .foo(Int)". Since your examples use the associated value label as the
name of the value inside the body, how would you handle those label-less
values?
>>
>>
>> On Mon, Jan 9, 2017 at 1:06 PM Tim Shadel <timshadel@gmail.com> wrote:
>> There are examples of associated values in the proposed syntax. Which
parts should I provide more detail on?
>>
>>> On Jan 9, 2017, at 1:43 PM, Tony Allevato via swift-evolution < > swift-evolution@swift.org> wrote:
>>>
>>> While I do like the consolidated syntax more than most of the
alternatives I've seen to address this problem, any proposed solution also
needs to address how it would work with cases that have associated values.
That complicates the syntax somewhat.
>>>
>>>
>>> On Mon, Jan 9, 2017 at 12:37 PM Sean Heber via swift-evolution < > swift-evolution@swift.org> wrote:
>>>
>>>> On Jan 9, 2017, at 2:28 PM, Guillaume Lessard via swift-evolution < > swift-evolution@swift.org> wrote:
>>>>
>>>>
>>>>> On 9 janv. 2017, at 10:54, Tim Shadel via swift-evolution < > swift-evolution@swift.org> wrote:
>>>>>
>>>>> Enums get large, and they get complicated because the code for each
case gets sliced up and scattered across many functions. It becomes a "one
of these things is not like the other" situation because writing functions
inside enums is unlike writing functions in any other part of Swift code.
>>>>
>>>> The problem I see with this is that enums and their functions
inherently multiply each other. If I have 3 cases and 3 functions or
properties, there are 9 implementation details, no matter how they're
organized. There can be 3 functions/properties, each with a 3-case switch,
or there can be 3 enum cases each with 3 strange, partial
functions/properties.
>>>>
>>>> I can see why someone might prefer one over the other, but is either
way truly better? The current way this works at least has the merit of not
requiring a special dialect for enums.
>>>
>>> I’m not sure how to argue this, but I feel pretty strongly that
something more like this proposed organization *is* actually better. That
said, I do not think this conflicts with the current design of enums,
however, so this is likely purely additive. The current design makes some
situations almost comically verbose and disorganized, IMO, but it *is*
right for other situations. We may want to have both.
>>>
>>> l8r
>>> Sean
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution@swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution@swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>
>

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Anton Zhilin) #20

Wouldn’t protocols be a better solution in this case? If little to no logic
can be shared between enum cases, why have the enum in the first place?

Your variant:

protocol State {

    mutating func react(to event: Event)
}
enum AuthenticationState: State, CustomStringConvertible {

    case invalid {
        var description: String { return "Authentication invalid." }

        mutating func react(to event: Event) {
            switch event {

            case let login as UserLoggedIn:
                self = .validated(token: login.token)
            default:
                break
            }
        }
    }

    case expired(_ expiration: Date) {
        var description: String { return "Authentication expired at
\(expiration)." }

        mutating func react(to event: Event) {
            switch event {
            case let refreshed as TokenRefreshed:

                self = .validated(token: refreshed.token)

            default:
                break
            }
        }
    }

    case validated(token: String) {

        var description: String { return "The authentication token is
\(token)." }

        mutating func react(to event: Event) {
            switch event {

            case let expiration as TokenExpired:
                print("Expiring token: \(token)")
                self = .expired(expiration.date)
            case _ as TokenRejected:
                self = .invalid
            case _ as UserLoggedOut:
                self = .invalid
            default:
                break
            }
        }
    }

}

My suggestion:

public protocol State {

    mutating func react(to event: Event)
}

@closed protocol AuthenticationState : State, CustomStringConvertible { }
struct InvalidAuthenticationState : AuthenticationState {
    var description: String { return "Authentication invalid." }

    mutating func react(to event: Event) {
        switch event {

        case let login as UserLoggedIn:
            self = .validated(token: login.token)
        default:
            break
        }
    }
}
struct ExpiredAuthenticationState : AuthenticationState {
    var expiration: Date

    var description: String { return "Authentication expired at
\(expiration)." }

    mutating func react(to event: Event) {
        switch event {
        case let refreshed as TokenRefreshed:

            self = .validated(token: refreshed.token)

        default:
            break
        }
    }
}
struct ValidatedAuthenticationState : AuthenticationState {
    var token: String

    var description: String { return "The authentication token is \(token)." }

    mutating func react(to event: Event) {
        switch event {

        case let expiration as TokenExpired:
            print("Expiring token: \(token)")
            self = .expired(expiration.date)
        case _ as TokenRejected:
            self = .invalid
        case _ as UserLoggedOut:
            self = .invalid
        default:
            break
        }
    }
}

If AuthenticationState is not public, then compiler can make an
optimization and turn existentials and indirect calls into an enum,
essentially.
We can even split public protocols into open and public, as we did with
classes, to allow for expressing this intent more explicitly.

But pattern matching on structs is impossible—we can change that with a
separate proposal. For example, we can allow destructuring
structs/enums/classes by any combination of their properties:

struct S {
    var foo: Int
    var bar: Double
    var buz: String { return "42" }
}
let s = S(foo: 42, bar: 42.0)
let S(foo: x, buz: z) = s