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

Tim Shadel timshadel at gmail.com
Tue Jan 10 19:07:30 CST 2017


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 <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.

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

### Basic example

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

```swift
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:

```swift
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.

```swift
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:

```swift
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.

```swift
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:

```swift
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.

```swift
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 at 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 at gmail.com <mailto:timshadel at gmail.com>> wrote:
> Yeah, that's much nicer than what I just sent! :-D
> 
> > On Jan 9, 2017, at 2:16 PM, Sean Heber <sean at fifthace.com <mailto:sean at 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 at gmail.com <mailto:tony.allevato at 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 at gmail.com <mailto:timshadel at 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 at swift.org <mailto:swift-evolution at 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 at swift.org <mailto:swift-evolution at swift.org>> wrote:
> >>>
> >>>> On Jan 9, 2017, at 2:28 PM, Guillaume Lessard via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
> >>>>
> >>>>
> >>>>> On 9 janv. 2017, at 10:54, Tim Shadel via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at 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 at swift.org <mailto:swift-evolution at swift.org>
> >>> https://lists.swift.org/mailman/listinfo/swift-evolution <https://lists.swift.org/mailman/listinfo/swift-evolution>
> >>> _______________________________________________
> >>> swift-evolution mailing list
> >>> swift-evolution at swift.org <mailto:swift-evolution at swift.org>
> >>> https://lists.swift.org/mailman/listinfo/swift-evolution <https://lists.swift.org/mailman/listinfo/swift-evolution>
> >>
> >
> 

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170110/b78799d8/attachment.html>


More information about the swift-evolution mailing list