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

Joshua Alvarado alvaradojoshua0 at gmail.com
Mon Apr 24 16:50:18 CDT 2017


>    func handle() {
>         switch self {
>         case .thingOne<T>(let s):
>             if T is String {
>                 // s is of type String
>             } else {
>                 // default case!
>             }
>         case .thingTwo<U>(let i):
>             if U is Int {
>                 // i is an Int
>             } else {
>                 // default case!
>             }
>         }
>     }

This would definitely be the default case if the generics don't have any constraints on them but the proposal is to make generics in enumerated better so that the above won't be needed they will just be used based on their constraints and no need to type cast (that would be the goal). 

Alvarado, Joshua

> On Apr 24, 2017, at 3:46 PM, Jaden Geller <jaden.geller at gmail.com> wrote:
> 
> 
>> On Apr 24, 2017, at 2:33 PM, Joshua Alvarado <alvaradojoshua0 at gmail.com> wrote:
>> 
>> Hmmm interesting point it would be good to consider this and get to an implementation that helps cover both or just implement the desired feature and in the future add more resiliency. 
>> 
>> Alvarado, Joshua
>> 
>>> On Apr 24, 2017, at 2:41 PM, Jaden Geller <jaden.geller at gmail.com> wrote:
>>> 
>>> 
>>>> On Apr 24, 2017, at 1:34 PM, Joshua Alvarado via swift-evolution <swift-evolution at swift.org> wrote:
>>>> 
>>>> Well in your case thing one and thing two will be the same type as you are using the same T generic type on both.
>>>> 
>>>> To achieve your case you can do an extension on the enum and use two different generics:
>>>> 
>>>> enum Thing {
>>>>     case thingOne<T>(T)
>>>>     case thingTwo<U>(U)
>>>> }
>>>> 
>>>> extension Thing where T == String, U == Int {
>>>>    func handle(thing: Thing) {
>>>>     switch thing {
>>>>     case thingOne(let s):
>>>>         // s is type String
>>>>         
>>>>     case thingTwo(let i):
>>>>         // i is an Int
>>>>     }
>>>>    }
>>>> }
> 
> I think it’s important to not that the above example (without generic information carried with the type) should not be able to provide an extension that constrains the type. The problem is that, since we don’t know what `T` and `U` actually are for an arbitrary instance at compile-time, we cannot ensure that `handle` is not called when `T` and `U` don’t match the extension. We’d have to require an implementation of `handle` for every possible type. I guess it _could_ be reasonable to allow such a constrained extension if we already defined `handle` for any `T` and `U`, but this implementation would probably be simple (and equivalent after optimization):
> 
> ```
> enum Thing {
>     case thingOne<T>(T)
>     case thingTwo<U>(U)
>     
>     func handle() {
>         switch self {
>         case .thingOne<T>(let s):
>             if T is String {
>                 // s is of type String
>             } else {
>                 // default case!
>             }
>         case .thingTwo<U>(let i):
>             if U is Int {
>                 // i is an Int
>             } else {
>                 // default case!
>             }
>         }
>     }
> }
> ```
> 
> Cheers,
> Jaden Geller
> 
>>>> 
>>>> This can actually be achieved in Swift 3.1, you can run this in a playground.
>>> 
>>> This is not quite the same. In the original example, the type information was thrown away after an instance of type `Foo` was constructed.
>>> 
>>> Here’s an example that wouldn’t work in your model:
>>> 
>>> ```
>>> enum Whatever {
>>>     case some<T>(T)
>>> }
>>> 
>>> var x: Whatever = .some(3)
>>> x = .some([1, 2, 3])
>>> ```
>>> 
>>> If `Whatever` was generic in `T`, then a variable can only store case payloads with that specific type. I guess this could be worked around by introducing a `AnyWhatever` protocol, but still, it’s not an identical feature.
>>> 
>>> That said, it might be true that this isn’t actually a desirable feature. We ought to definitely consider the implications of it and any alternatives. Thanks for mentioning this, it’s a decent workaround dependent on what you’d like to accomplish!
>>> 
>>> Cheers,
>>> Jaden Geller
>>> 
>>>> 
>>>> enum Foo<T, U> {
>>>>     case bar(obj: T)
>>>>     case baz(obj: U)
>>>> 
>>>>     func handle() {
>>>>         switch self {
>>>>         case .bar(obj: let x):
>>>>             break
>>>>         case .baz(obj: let y):
>>>>             break
>>>>         }
>>>>     }
>>>> }
>>>> 
>>>> extension Foo where T == String, U == Int {
>>>>     func handle() {
>>>>         switch self {
>>>>         case .bar(obj: let str):
>>>>             print(str)
>>>>         case .baz(obj: let aNum):
>>>>             print(aNum)
>>>>         }
>>>>     }
>>>> }
>>>> 
>>>> let foo = Foo<String, Int>.baz(obj: 1)
>>>> foo.handle() // prints 1
>>>> 
>>>> 
>>>> 
>>>>> On Mon, Apr 24, 2017 at 2:15 PM, Kevin Nattinger <swift at nattinger.net> wrote:
>>>>> This makes it more convenient to create them, sure, but how would you pass them around or extract the value in a type-safe manner?
>>>>> 
>>>>> e.g. now I can write:
>>>>> enum Thing<T, U> {
>>>>>     case thingOne(T)
>>>>>     case thingTwo(U)
>>>>> }
>>>>> 
>>>>> // How do I require thingOne<String> or thingTwo<Int>?
>>>>> func handle(thing: Thing<String, Int>) {
>>>>>     switch thing {
>>>>>     case .thingOne(let s): print("string \(s)")
>>>>>     case .thingTwo(let i): print("int \(i)")
>>>>>     }
>>>>> }
>>>>> 
>>>>> With your proposed syntax:
>>>>> 
>>>>> enum Thing {
>>>>>     case thingOne<T>(T)
>>>>>     case thingTwo<T>(T)
>>>>> }
>>>>> 
>>>>> func handle(thing: Thing) {
>>>>>     switch thing {
>>>>>     case thingOne(let s):
>>>>>         // What is the type of s?
>>>>>     case thingTwo<Int>(let i):
>>>>>         // is it even possible to write an exhaustive switch?
>>>>>     }
>>>>> }
>>>>> 
>>>>> 
>>>>> 
>>>>>> On Apr 24, 2017, at 6:57 AM, Joshua Alvarado via swift-evolution <swift-evolution at swift.org> wrote:
>>>>>> 
>>>>>> Here is my pitch on adding generics to enum cases and not to the enum type itself. Let me know if you have an improvements or modifications lets open it to discussion thank you swiftys! :)
>>>>>> Enum with generic cases
>>>>>> Proposal: SE-NNNN
>>>>>> Authors: Joshua Alvarado
>>>>>> Review Manager: TBD
>>>>>> Status: PITCH
>>>>>> During the review process, add the following fields as needed:
>>>>>> 
>>>>>> Decision Notes: Rationale, Additional Commentary
>>>>>> Bugs: SR-NNNN, SR-MMMM
>>>>>> Previous Revision: 1
>>>>>> Previous Proposal: SE-XXXX
>>>>>> Introduction
>>>>>> 
>>>>>> This proposal adds a change to the enumeration type that allows an enum case to cast a generic on its associated value.
>>>>>> 
>>>>>> Swift-evolution thread: Discussion thread topic for that proposal
>>>>>> 
>>>>>> Motivation
>>>>>> 
>>>>>> Enums currently support generics, but they are added onto the type itself. This can cause adverse syntax when implementing generics for associated values to be stored along each case. The enum case holds the associated value (not the enum type itself) so should create its own value constraints.
>>>>>> 
>>>>>> Proposed solution
>>>>>> 
>>>>>> The generic is to be casted on the case of the enum and not on the enum itself.
>>>>>> 
>>>>>> Detailed design
>>>>>> 
>>>>>> Current implementation:
>>>>>> 
>>>>>> // enum with two generic types
>>>>>> enum Foo<T: Hashable, U: Collection> {
>>>>>>     case bar(obj: T)
>>>>>>     case baz(obj: U)
>>>>>> }
>>>>>> 
>>>>>> // U is to be casted but it is not even used
>>>>>> let foo: Foo<String, [String]> = .bar(obj: "hash")
>>>>>> 
>>>>>> // Creating an optional enum, the generics have to be casted without a value set
>>>>>> // The casting is really not needed as the values should be casted not the enum
>>>>>> var foo1: Foo<String, [String]>?
>>>>>> 
>>>>>> // Collections don’t look great either
>>>>>> var foos = [Foo<String, [String]>]()
>>>>>> foos.append(.bar(obj:"hash"))
>>>>>> Proposed solution
>>>>>> 
>>>>>> enum Foo {
>>>>>>     case bar<T: Hashable>(obj: T)
>>>>>>     case baz<U: Collection>(obj: U)
>>>>>> }
>>>>>> 
>>>>>> // generic type inferred on T
>>>>>> var foo: Foo = .bar(obj: "hash") 
>>>>>> 
>>>>>> // doesn’t need to cast the generic on the optional enum
>>>>>> // the associated value will hold the cast
>>>>>> var foo1: Foo? 
>>>>>> 
>>>>>> // This also gives better syntax with collections of enums with associated types
>>>>>> var foos = [Foo]()
>>>>>> foos.append(.bar(obj: "hey")
>>>>>> Source compatibility
>>>>>> 
>>>>>> This may cause subtle breaking changes for areas in code with generic enum cases. The compiler could help with the change by finding the associated generic and updating the case with the new syntax.
>>>>>> 
>>>>>> Alternatives considered
>>>>>> 
>>>>>> An alternative would be to extend the associatedtype keyword to the enum type.
>>>>>> 
>>>>>> enum Foo {
>>>>>>     associatedtype T = Hashable
>>>>>>     case bar(obj: T)
>>>>>> }
>>>>>> 
>>>>>> Copy of proposal can be found here Swift proposal on github
>>>>>> 
>>>>>> -- 
>>>>>> Joshua Alvarado
>>>>>> alvaradojoshua0 at gmail.com
>>>>>> _______________________________________________
>>>>>> swift-evolution mailing list
>>>>>> swift-evolution at swift.org
>>>>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>>> 
>>>> 
>>>> 
>>>> 
>>>> -- 
>>>> Joshua Alvarado
>>>> alvaradojoshua0 at gmail.com
>>>> _______________________________________________
>>>> swift-evolution mailing list
>>>> swift-evolution at swift.org
>>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>> 
> 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170424/da771253/attachment.html>


More information about the swift-evolution mailing list