enumCase[case: Foo.bar(name:)] is perfectly valid and will return a String as expected, as well as let value: Int? = enumCase[case: Foo.bar] will work. In my library (I've built a library out of this conversation, I called it EnumKit) I discourage to use overloaded cases because I found that for some reason, in some edge case Xcode crashes (yeah... Xcode, not the program... in build time :D), but I guess that this depends on the fact I use mirroring (that I discourage if this feature would ever be considered to be on language level)
Compound identifiers are left behind on purpose. The result of the expression is a tuple. We don't offer compound identifiers for tuple properties in structs. I don't see why enums should be any different.
The main problem I see with generating key paths is that we are trying to force a square in a circle. If we were treating cases like variables of a structs, and generate key paths named after cases, we get into the issue that for structs two variables of the same name with different type are just not possible.
The behavior of enum cases is way more similar to functions for which doesn't exist the concept of key path.
I wrote a medium article about it. At the end of it I exposes the cases where my library doesn't work. They all depend on the fact that I must use mirroring.
To provide a more real-world example, we covered a compelling reason for enum key paths/properties this week on Point-Free:
Though this video is behind a paywall, our code is always open source:
In particular we show how reducer architecture can be modularized and reconstituted using a couple succinct operations:
An operation that converts Reducer<LocalState, LocalAction> to Reducer<GlobalState, GlobalAction>
An operation that combines reducers that work on the same state and action type together
The first operation cannot succinctly transform LocalActions into GlobalActions in a performant manner without compiler help and enum properties and key paths perfectly handle this today (with a little boilerplate that ideally the compiler handles for us).
Not really. The behavior of enum cases is to contain data, much like properties. Also: method key paths have been suggested by core team members that worked on the original key path implementation. So, potential future hierarchy:
I'm really happy to see so many alternatives to the problem. It makes at least a strong point about the fact that in many we feel the problem exists, even if we can't agree yet on a solution. We will in time. Let's keep going.
You can definitely accomplish the same thing (and we'll probably use a similar solution while we wait for the compiler to catch up), but I hope you can see that the dual nature of structs and enums and data access lends itself nicely to a shared construct like key paths. And I hope you'll entertain the idea that there's no reason to force enums to opt into such a universal construct (or introduce a separate syntax when a new key path type will do so just fine).
After all, if we don't generate properties, it's not dissimilar from what I'm proposing here. Instead of a function we would be using a new EnumKeyPath. I'd be happy with it.
I'm still trying to figure out everything in this thread. Something I found out about in another thread was "optics," a general term for "lenses" and "prisms." A lens is a field (or nested field) of a product type. A prism is an optional access point to a case of a sum type (or an aspect of an unspecified algebraic type that is only sometimes valid). I'm still trying to grok optics, but your idea seems like it's dabbling in the same area as prisms. (The article I just re-read noted that a lens is just a prism that the compiler knows is 100% reliable.)
I agree, like another poster mentioned, that this should be part of the core language. A protocol isn't appropriate because there shouldn't be customization. It's like a switch; an enum type cannot opt-out of being targeted by a switch, it cannot partially opt-out, and it cannot provide a quirky override of how the switch process it (It's fixed by someone on the Swift development team.). The switch statement's calculus over enum types is total and absolute, and the same should apply to this facility. Built-in operations using (something like) key paths to reference fields is probably better than dumping a bunch of (nested) named members into a type that share the namespace.
There's been discussion of write access. For read access, we can do:
let x: MyCaseType? = myVariable[use: \MyEnum.myCase.self]
(where self references the entire payload tuple, or Void for singular cases). But what happens when writing:
enum MyEnum {
case one
case two(a: Int, b: Double)
}
var y: MyEnum = .one
y[use: \MyEnum.two.b] = 4.5
The object's case was reset, but we didn't initialize the entirety of the .two tuple, just one member. Swift deliberately does not have defaulted initialization, so the .a payload member has an illegal garbage value, and shouldn't be allowed at compile-time if possible, or run-time at worst. Are we going to restrict writing only when the leaf case is changed in whole and no intermediate case in the chain has skipped sibling members?
I'm happy with having KeyPaths as long as they are of a different type: EnumKeyPath.
You might want the same functionality for an arbitrary OptionSet, hence the protocol. Functions of the protocol can still only be used on concrete types, therefore it all remains strongly typed. You would write cases like you do for a switch already.
I don't think the compiler should stop you from doing that, and I don't possibly see how it could, given the fact that y can be any case of MyEnum.
y[case: \.two.b] = 4.5
// is a shortcut to
if case let .two(a, _) = y {
y = .two(a: a, b: 4.5)
}
while the expression y[case: \.one] = 4.5 Should be forbidden by the compiler because one does not have an associated value that can possibly contain 4.5.
Note that while we are trying to give to enums capabilities today available only to structs (KeyPath), enums and structs are objectively different. Overloads on structs variable is not possible, therefore I believe that enum cases should not and cannot be considered as structs properties. They are more similar to functions for this aspect. There is no concept of KeyPath for functions yet.
enum Foo {
case bar(a: Int)
case bar(a: Int, b: Int)
case baz(String)
case baz(String, String)
case bla
}
This is a perfectly valid enum. \.bar or \.bar.a is ambiguous, Foo.bar(a:) (possible today) isn't.
More seriously, key paths come with an established class hierarchy that has been carefully designed. Asking for another type out of the blue, without any justification, is odd and looks not constructive - at least to me. Not being familiar with key paths is not really an excuse. Why not, instead, becoming fluent with them? If they fit, good. If they don't fit, then we must know why.
Carefully designed for properties. At the time enum were not on the table, as far as I know. Enum cases are not properties and because they allow overloading they are not even alike to properties. They are more similar to functions, imho.
I agree. That's the reason why this pitch was not born as a spin-off of the KeyPaths pitch, and it was proposed as an alternative.
They don't fit, because to disambiguate overloading cases you need "more" than just the case name. You need specific informations about the payload, such as types and/or labels for each element of the tuple that composes the associated value. So you either need a different "KeyPaths alike" construct, or you need to accept that you can't have KeyPaths. Maybe having something completely different (say pattern).
We can also accept the idea that product types and sum types in swift are just different. What you have with structs does not necessarily apply to enums, and the "do nothing" becomes a possibility. What the community wants? This is the ultimate question in my opinion.
Do we want to access associated values with better ergonomics than switch and pattern matching?
Do we want KeyPaths, or it's better to accept the fact that cases are not properties, therefore keypaths are not a good fit?
Answering these questions will help to go forward for a formal proposal to achieve a better ergonomics if the answer to the question 1. is yes.
I do think we need better ergonomics
I think KeyPaths are not the way, but if the community has a different feeling I'm ready to accept that.
Overloading cases makes the difference between cases and structs properties that remark the importance of not having KeyPaths for enums the way we know them today.
The above sample code from @stephencelis uses an EnumKeyPath type because Stephen could not rely on language-provided key paths: they don't provide any public initializer. Is the presence of this type the side effect of a constraint that could be lifted eventually, or something that is absolutely necessary?
Let's see! I apologize in advance: this post is long, and not even complete.
The existing hierarchy of key paths is the following:
Our first desire is that the key path subscript returns the "payload" if the enum has the matching case. This optionality is expressed with the standard Optional type:
DESIRE 1
We can extract an optional Int with the key path \MyEnum.value:
let e: MyEnum
e[keyPath: \.value] // Type: Int?
We deduce from the fundamental key path subscripts: \MyEnum.value has the type KeyPath<MyEnum, Int?>. And we can write a naive hypothesis: a key path has the type KeyPath<MyEnum, T?> where T is the type of the payload.
Is it consistent? With the void case, it's quite OK:
Is it OK if the type of a key path to a case that contain N values is identical to a type of a key path to a case that contains the same N values, wrapped in a tuple? Despite the fact that the static factory methods for those cases don't have the same types?
It is a difficult question, if only because we start to blur the definition of the "type of the payload". It would be nice if it could be unambiguously defined. It looks it can't. We start to understand that enum key paths may have to use their own, ad-hoc, definition of the type of the payload, driven by key path usage.
Let's put the question on hold until we get more information. We still have never, empty, and namedValue to deal with.
// Compiler error: key path cannot refer to value-less enum case
\MyEnum.empty
QUESTION 2
How should we deal with key paths to a non-existent payload? The same as Void payloads? With a boolean? With a compiler error? In another way?
Remains the case of namedValue. The language has an ambiguous relation to single-valued tuples. They exist, burried inside the language guts, but they can't be expressed in userland. We have no choice:
I'll thus suppose that tuple elements are allowed in key paths, even if they are not yet. My goal is to look for inconsistencies between enums and key paths.
This is wild because this does not match at all the type of the basic key path, where there is no tuple and no a member is sight:
\MyEnum.namedValue // Type: KeyPath<MyEnum, Int?>
I don't know enough how key paths are implemented, and if the compiler could make it work despite the abscence of the required information at the type level.
QUESTION 3
Do we allow the tuple extraction syntax for key paths to payload made of a single named value?
Sorry for this long post. This is the end of this first exploration, which covers the read-only side. SE-0155 has not been covered. Writable key paths have not been covered.
Yet I have one preliminary conclusion: no contradiction has been exhibited between the built-in read-only KeyPath type and enums. There is no evidence, in this exploration, that we need a new, dedicated, EnumKeyPath type.
Some questions have been asked, and they may hide traps. On top of that, the third desire expressed above, about payloads that contain a single named value (\MyEnum.namedValue?.a), may require some special compiler support. But this desire can be discarded as not-implementable. And the questions require more exploration before they can turn into a deal-breaker for KeyPath and enums.
enum MyEnum {
case empty
case void(Void)
case never(Never)
case value(Int)
case namedValue(a: Int)
case values(Int, String)
case namedValues(a: Int, b: String)
case tuple((Int, String))
case namedTuple((a: Int, b: String))
}
Computed properties get for free a keyPath that in this case happens to be the same as the KeyPath that would be generated for the value case. Should the compiler stop you from creating computed vars with the same name as the existing case? What about existing code where this computed properties are pretty common?
having different KeyPaths types and subscript would fix this.
extension MyEnum {
var value: Int? {
self[case: \.value] // case associated type
}
}
e[keyPath: \.value] // property
e[case: \.value] // case associated type