[swift-evolution] Pre-proposal: CaseEnumerable protocol (derived collection of enum cases)

Zef Houssney zefmail at gmail.com
Thu Jan 21 08:59:26 CST 2016


Regarding support for enums with associated values, I’d like to argue for the idea of returning the constructors along side the instances of simple enum cases.

I find that it’s common to have enums that have mostly simple cases, but a couple with associated values that provide more dynamic customization. I would hate to be unable to include the cases with associated values in order to get conformance for this feature. Here’s an example of something like that. The concept is that you have an enum that defines a number of built-in color schemes, but provides a mechanism for users to dynamically create their own color schemes:

https://gist.github.com/zef/c6069ed4ed11e41661bf

I have other real-world ideas for where this would be useful too. This is a contrived example that is simplified such that it would be just as easy to implement the result without the constructor being included in the list of cases (or allValues), but there are use-cases for where there are multiple cases with the same constructor signature where it would be valuable for them to be included.


Joe Groff said:

> Asking for the collection of case constructors seems like a different thing to me—you're getting a [(Payload) -> Self] rather than a [Self] collection.


I agree that this is kind of a problem, but I find this a better tradeoff than the alternatives, which as I see them are:

1. Don’t support enums with disparate constructor signatures at all (currently proposed — compiler error for these)
2. Allow conformance for all enums, but omit all cases that have associated values (more useful than the proposed solution, but confusing and not ideal)
3. A combination of 1 and 2, where some cases with associated values can potentially have multiple cases included, or none at all (confusing and potentially annoying to work around if it’s not what you want)

Omitting the values and getting back a collection of [Self] would be easy enough anyway: MyEnum.cases.flatMap { $0 as? MyEnum }


Here’s what I like about this approach:

- This would allow all enums be able to conform, not just ones where every case has the same type.
- The array consists of a 1-to-1 mapping regardless of the type of case. This could be very valuable and powerful compared to some of the other ideas that could omit or have multiple entries for a single case.
- As a base implementation, all the other ideas for how to deal with cases with associated values can be implemented on top of this foundation by developers, whereas the other approaches are somewhat more limiting and would not cover the use case I have above.


Downsides I can see to this approach:

- For enums that have disparate constructor signatures, [Any] would have to be the return value of cases, which doesn’t seem ideal because the cases array would always require some kind of manual manipulation to be useful. Would it even be possible for cases to return the [EnumType] for simple enums but [Any] for more complex ones? Is there a better type than [Any] that could be used?

- There isn’t currently a way in the type system to know what case you are working based on the constructor. For instance say two cases both have a constructor of (String), you can’t determine which case you are working on until you call the constructor to instantiate the case. This could be done by looking at the index of the constructor in the cases array, but that is brittle and messy. If there was a way to determine that more dynamically, this would be more powerful and easier to use.


FYI, some of this content is taken from a post where I wrote about this, but it was just before Christmas and didn’t really get any feedback: https://lists.swift.org/pipermail/swift-evolution/Week-of-Mon-20151221/004289.html




> On Jan 20, 2016, at 10:40 PM, Slava Pestov via swift-evolution <swift-evolution at swift.org> wrote:
> 
>> 
>> 
>> Support for enum case names. It would be useful to get case names even for enums which have integer rawValues. This could be part of the existing reflection APIs, or it could take the form of derived implementations of StringLiteralConvertible/CustomStringConvertible.
>> 
>> 
> 
> I’d love to see reflection move to an opt-in model, with a compiler flag enabling reflection for all types in a module, and a protocol to opt-in conditionally otherwise.
>> Support for enums with associated values.
>> 
>> 
> 
> This seems tricky to use statically in the general case, as you see below. Do you have any good examples in mind? It seems that case enumeration only really makes sense for types with a finite number of elements, and not when you have associated values.
> 
> Perhaps it should only work if the associated values are themselves enums, and they can be enumerated recursively? But even that seems overkill, and something that people should implement themselves if they need it.
> 
>> If Swift had anonymous sum types like A | B | C, then E.cases could vend elements of type A->E | B->E | C->E.
>> 
>> 
> 
> As part of write reflection, it would make sense to expose constructor functions [Any -> E?] that perform a dynamic type check, failing with nil.
> 
> Imagine if you could reflect an enum of type T, and get an array of [EnumCase<T>]:
> 
> enum EnumCase<T> {
> 	// constructor makes a value: Payload -> T
> 	// projection tests a value, returning payload if its that case, or nil: T -> Payload?
> 	// type is the runtime type of the payload
> 	case PayloadCase(name: String, constructor: Any -> T, projection: T -> Any?, type: Any.Type)
> 	case EmptyCase(name: String, value: T)
> }
> 
> Then you can get representative values of all cases by mapping over this array, returning the ‘value’ element of an EmptyCase.
> 
> 
>> enum Expr { case Apply(Expr, Expr), Tuple(Expr, Expr), Literal(Int) }
>> extension Value: CaseEnumerable {}
>> 
>> // This example is pretty contrived, but illustrates the functionality.
>> let fortyTwos = Expr.cases.map {
>>    // $0 is of type `Int -> Expr | (Expr, Expr) -> Expr`
>>    switch $0 {
>>    case let lit as Int -> Expr:  // handles .Literal
>>        return lit(42)
>>    case let bin as (Expr, Expr) -> Expr:  // handles .Apply and .Tuple
>>        return bin(.Literal(42), .Literal(42))
>>    // all cases are covered
>>    }
>> }
> 
> I think in this example program, it would make more sense to define these data types:
> 
> enum BinaryExprKind { cae Apply, Tuple }
> enum Expr { case Binary(Kind, Expr, Expr), Literal(Int) }
> 
> You can still enumerate the cases of BinaryExprKind dynamically, but destructuring Expr requires a switch.
>> Support for generic enums.
>> 
>> CaseEnumerable could be conditionally supported depending on the generic argument(s). A great example would be Optional:
>> 
>> enum MyEnum: CaseEnumerable {}
>> extension Optional: CaseEnumerable where Wrapped: CaseEnumerable {}
>> 
>> // Optional<MyEnum>.cases effectively contains `MyEnum.cases + [.None]`
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution at swift.org <mailto:swift-evolution at swift.org>
>> 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/20160121/a74d1c08/attachment.html>


More information about the swift-evolution mailing list