Enum with generic cases

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.
- Declare a variable that can hold a thingOne<String> or thingTwo<Int> but nothing else.

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

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.

···

On Apr 24, 2017, at 2:38 PM, Kevin Nattinger <swift@nattinger.net> wrote:

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&gt; 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

1 Like

Yes, that was almost exactly my original example. My understanding of the proposal is that it will remove that capability, which I find completely unacceptable.

···

On Apr 24, 2017, at 3:16 PM, Jaden Geller <jaden.geller@gmail.com> wrote:

On Apr 24, 2017, at 2:38 PM, Kevin Nattinger <swift@nattinger.net <mailto:swift@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)!")
    }
}

Hi Kevin,

If that is what is being proposed, I agree that that is entirely unacceptable. I however did not understand the proposal to be removing that capability, but instead I understood that it was simply adding another. That is, `Thing<T1, T2>` would *still* be accepted. This feature is entirely additive.

It would be great if the original proposal author could confirm this.

Thanks,
Jaden Geller

···

On Apr 24, 2017, at 3:21 PM, Kevin Nattinger <swift@nattinger.net> wrote:

On Apr 24, 2017, at 3:16 PM, Jaden Geller <jaden.geller@gmail.com <mailto:jaden.geller@gmail.com>> wrote:

On Apr 24, 2017, at 2:38 PM, Kevin Nattinger <swift@nattinger.net <mailto:swift@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)!")
    }
}

Yes, that was almost exactly my original example. My understanding of the proposal is that it will remove that capability, which I find completely unacceptable.

I once put together a sketch for a proposal for GADTs. I wouldn't recommend
it be submitted for review today (or ever, really), but it's there if
anyone is interested.

Best,
Austin

···

On Mon, Apr 24, 2017 at 3:24 PM, Jaden Geller via swift-evolution < swift-evolution@swift.org> wrote:

On Apr 24, 2017, at 3:21 PM, Kevin Nattinger <swift@nattinger.net> wrote:

On Apr 24, 2017, at 3:16 PM, Jaden Geller <jaden.geller@gmail.com> wrote:

On Apr 24, 2017, at 2:38 PM, Kevin Nattinger <swift@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)!")
    }
}

Yes, that was almost exactly my original example. My understanding of the
proposal is that it will remove that capability, which I find completely
unacceptable.

Hi Kevin,

If that is what is being proposed, I agree that that is entirely
unacceptable. I however did not understand the proposal to be removing that
capability, but instead I understood that it was simply adding another.
That is, `Thing<T1, T2>` would *still* be accepted. This feature is
entirely additive.

It would be great if the original proposal author could confirm this.

Thanks,
Jaden Geller

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

Is there any update on this?

Seems like the idea to discuss it once Swift 4 is released didn't quite materialize...

2 Likes

I'm slowly trying to learn enough to work on an implementation. I have an idea of what the syntax would look like but… implementations are harder to come by.

4 Likes

I think the alternative syntax using an associated type is better because the behaviour is much more like an associated type where you have to use constraints to do anything with the values.

As an aside I would like to see associated types adding to structs, enums, and classes and generics to protocols so that you can pick the best solution. This is something Scala has and can be very handy.

2 Likes

+1 for the idea to expand associated type and generics. Just wondering what disadvantages this may have, meaning what's the reason it isn't here already. Seems like a design decision to me.

Adding associated type to struct at least wouldn't make sense as inheritance isn't possible there. Adding it to classes would possibly not fit Swift's design promoting use of protocols instead of inheritance.

Not having thought about it in detail, adding generics to protocol however makes sense to me. Correct me if I'm wrong.