[swift-evolution] [Pitch] Enum with generic cases

Jaden Geller jaden.geller at gmail.com
Mon Apr 24 17:16:40 CDT 2017


> On Apr 24, 2017, at 2:38 PM, Kevin Nattinger <swift at nattinger.net> wrote:
> 
>> 
>> How can I improve your understanding?
>> 
> 
> 
> Given the enum I was using earlier:
> 
> enum Thing {
>     case thingOne<T>(T)
>     case thingTwo<T>(T)
> }
> 
> - Write a function that takes a thingOne<String> or thingTwo<Int> but nothing else.

This isn’t possible since generic types introduced on cases are erased in the type of `Thing`.

We can actually already achieve what you want by moving the generics onto the type itself, and this is already possible in Swift! No new features are necessary.

```
enum Thing<T1, T2> {
    case thingOne(T1)
    case thingTwo(T2)
}

func test(_ x: Thing<String, Int>) {
    switch x {
    case .thingOne(let s):
        print("The string has value \(s)!")
    case .thingTwo(let i):
        print("The int has value \(i)!")
    }
}
```

> - Declare a variable that can hold a thingOne<String> or thingTwo<Int> but nothing else.

With our new definition, we can write:

```
var x: Thing<String, Int>
```

This variable can be initialized with a `thingOne` containing a `String` payload or a `thingTwo` containing an `Int` payload.

> Or explain why something that is widely used, trivial to write, and necessary for type safety should be removed from the language.

As I just explained, what you desire is *already possible* in the language. This new features does not address your use case. Instead, this feature allows for type erasure of enum cases. This is pretty similar to storing an existential as a payload. I’m not even sure if it’s sufficiently different to justify its addition to the language.

The original non-generic definition of `Thing` with generic cases is essentially equivalent to storying `Any` as the case payloads. We can make this more useful by using protocols to constrain the payload type.

I think this proposal doesn’t really add any useful new capabilities without the ability to constraint the return type. For example, these are nearly equivalent:

```
enum Key {
    case hashable<T: Hashable>(T)
    case any<T>(T)
}
```

vs.

```
enum Key {
    case hashable(AnyHashable)
    case any(Any)
}
```

Both of these definitions are nearly equivalent except the former introduces a new generic variable in scope when you match on a constrained case. Is this useful? I’m not sure. Is it breaking type safety? Absolutely not!! If I match the `hashable` case in a switch statement, it’s very similar to if I had written a function with the following signature:

```
func matchHashable<T: Hashable>(_ x: T) { ... }
```

Because of this, no type safety is lost in the switch statement.

—

I think a more interesting version of this proposal would be as follows: Allow where constraints and explicit return type annotations on case declarations. This is very similar to what a GADT <https://en.wikibooks.org/wiki/Haskell/GADT> allows you to do. Check out how cool this example is—and 110% type-safe!

Below is an example of a data structure representing a computation. We can inspect is since it is just data, but we can also evaluate it to see the result.

```
enum Expression<T> {
    case value(T)
    case isEqual(T, T) -> Expression<Bool> where T: Equatable
    case plus(T, T) -> Expression<T> where T: Numeric
    case and(Bool, Bool) -> Expression<Bool>
    case not(Bool) -> Expression<Bool>
    // etc.
    
    func evaluate() -> T {
        switch self {
        case .value(let x):
            return x
        case .isEqual(let lhs, let rhs):
            // We know that T: Equatable, so we can use `==`
            return lhs == rhs
        case .plus(let lhs, let rhs):
            // We know that T: Numeric, so we can use `+`
            return lhs + rhs
        case .and(let lhs, let rhs):
            return lhs && rhs
        case .not(let x):
            return !x
        }
    }
}

let x: Expression<Int32> = .plus(.value(3), .value(10))
let y: Expression<Int32> = .plus(.value(7), .value(6))
let z: Expression<Bool> = .isEqual(x, y)

print(z.evaluate()) // -> true
```

Notice that it is impossible to construct an `Expression` of a nonsensical type. Try to do this with the enums we have in Swift today. You’ll have to do some force casting of types. Not pretty.

—

I hope I was able to clarify some things for you. Let me know if you have any other questions.

Cheers,
Jaden Geller

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


More information about the swift-evolution mailing list