Consolidate Code for Each Case in Enum


(Tim Shadel) #1

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)"
    }
  
    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)"
    }
  
    <<< 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


(Rien) #2

I have mixed feelings about this. I would like to see a solution to the exploding switch statements in an enum, and we have talked about that before on this list. However the solution as suggested seems deficient to me.

First, A developer would have to look at an enum “extension” in order to be able to know what to do with the enum. And those extensions can be in different files? So at the very least the enum would need to carry the full spec of what it can do.

I also dislike the use of the keyword “extension” here. It does not extend anything imo.

And thirdly, I dislike having to repeat the function definitions for every enum case, that is even more verbose than “switch case”.

Other than that, I would support the simplification of enums as such :slight_smile:

Regards,
Rien

Site: http://balancingrock.nl
Blog: http://swiftrien.blogspot.com
Github: http://github.com/Swiftrien
Project: http://swiftfire.nl

···

On 07 Jan 2017, at 07:59, Tim Shadel via swift-evolution <swift-evolution@swift.org> wrote:

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)"
    }
 
    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)"
    }
 
    <<< 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
https://lists.swift.org/mailman/listinfo/swift-evolution


(Robert Widmann) #3

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> のメッセージ:

···

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)"
    }
 
    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)"
    }
 
    <<< 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
https://lists.swift.org/mailman/listinfo/swift-evolution


(David Waite) #4

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.

<musings>

I think this is the primary difference between a “tagged union” and a Swift enum - I might have a union of several discrete types in other languages, but in swift it is actually an enumeration of cases within a single type. The individual cases are not types.

This was actually one of the reasons that drove case labels and static values to stylistically change to lowercase in Swift 3 - to avoid building a misconception of enum cases being distinct types.

I’ve wanted this feature in the past as well, but wonder if at some point encouraging support for a distinct union of types makes more sense than expanding enum - say, a closed protocol only allowing implementation by specific types declared within its module.

-DW


(Derrick Ho) #5

Correct me if I am wrong but currently there are only two ways to extract
the value of an associated type.

// 1
switch n {
case foo(let value):
print(value)
}

// 2
if case foo(let value) = n {
print(value)
}

I think it would be nice to have another way. Maybe a tuple-like pattern.

let value = n.foo.0 // returns value as an optional.

If this can already be done, ignore me.

···

On Sun, Jan 8, 2017 at 2:20 AM Robert Widmann via swift-evolution < swift-evolution@swift.org> wrote:

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>
のメッセージ:

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)"

    }

    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)"

    }

    <<< 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
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) #6

It would be nice if for each enum case, there was a computed property
returning an optional. Then you could do:

enum FooBar {
    case foo(Int)
    case bar(Int)
}
let x = FooBar.foo(42)
x.foo //=> Optional(42)
x.bar //=> nil

This feature has been discussed many times, but each time it was considered
“out of scope”.


(Daniel Leping) #7

+1 as for compleax enums it really does get messy. Leave the current
approach in place, though.

I'm not sure the syntax is best though. Maybe we could just open a curly
brace right after the enum case declaration and put the specific function
there. Same way everything out of the braces is considered a _default_ case.

···

On Sun, 8 Jan 2017 at 14:12 Anton Zhilin via swift-evolution < swift-evolution@swift.org> wrote:

It would be nice if for each enum case, there was a computed property
returning an optional. Then you could do:

enum FooBar {

    case foo(Int)

    case bar(Int)

}

let x = FooBar.foo(42)

x.foo //=> Optional(42)

x.bar //=> nil

This feature has been discussed many times, but each time it was
considered “out of scope”.

_______________________________________________

swift-evolution mailing list

swift-evolution@swift.org

https://lists.swift.org/mailman/listinfo/swift-evolution