[swift-evolution] Consolidate Code for Each Case in Enum

Tim Shadel timshadel at gmail.com
Mon Jan 9 15:15:11 CST 2017


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

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 at 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 at 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 at 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 at swift.org> wrote:
>>> 
>>> On Jan 8, 2017, at 06:53, Karim Nassar via swift-evolution <swift-evolution at 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 at swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution at swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>> 
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution
> 



More information about the swift-evolution mailing list