Extract Payload for enum cases having associated value

Can built-in key paths deal with enums?

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:

All those can interact with actual values with the fundamental key path subscripts:

extension Any {
    subscript(keyPath path: AnyKeyPath) -> Any? { get }
    subscript<Root: Self>(keyPath path: PartialKeyPath<Root>) -> Any { get }
    subscript<Root: Self, Value>(keyPath path: KeyPath<Root, Value>) -> Value { get }
    subscript<Root: Self, Value>(keyPath path: WritableKeyPath<Root, Value>) -> Value { set, get }
}

OK, so let's start performing deductions. Let's start from a simple enum, remind some fundamentals about it, and give a few desiderata.

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))
}

// Fundamentals
MyEnum.empty       // Type: MyEnum
MyEnum.void        // Type: (Void) -> MyEnum
MyEnum.never       // Type: (Never) -> MyEnum
MyEnum.value       // Type: (Int) -> MyEnum
MyEnum.namedValue  // Type: (Int) -> MyEnum
MyEnum.values      // Type: (Int, String) -> MyEnum
MyEnum.namedValues // Type: (Int, String) -> MyEnum
MyEnum.tuple       // Type: ((Int, String)) -> MyEnum
MyEnum.namedTuple  // Type: ((a: Int, b: String)) -> MyEnum

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:

\MyEnum.void       // Type: KeyPath<MyEnum, Void?>
e[keyPath: \.void] // Type: Void?

The tuple cases should be no different. After all, a tuple is just a value, right?

\MyEnum.tuple            // Type: KeyPath<MyEnum, (Int, String)?>
\MyEnum.namedTuple       // Type: KeyPath<MyEnum, (a: Int, b: String)?>
e[keyPath: \.tuple]      // Type: (Int, String)?
e[keyPath: \.namedTuple] // Type: (a: Int, b: String)?

With compound enums that wrap several values, do we have a choice? How to express a list of values without a tuple?

// What else?
\MyEnum.values            // Type: KeyPath<MyEnum, (Int, String)?>
\MyEnum.namedValues       // Type: KeyPath<MyEnum, (a: Int, b: String)?>
e[keyPath: \.values]      // Type: (Int, String)?
e[keyPath: \.namedValues] // Type: (a: Int, b: String)?

Those key paths to compound values are identical to key paths to the same values, wrapped in a tuple. We have introduced our first inconsistency:

// Identical
\MyEnum.values      // Type: KeyPath<MyEnum, (Int, String)?>
\MyEnum.tuple       // Type: KeyPath<MyEnum, (Int, String)?>

// Different
MyEnum.values       // Type: (Int, String) -> MyEnum
MyEnum.tuple        // Type: ((Int, String)) -> MyEnum

// Identical
\MyEnum.namedValues // Type: KeyPath<MyEnum, (a: Int, b: String)?>
\MyEnum.namedTuple  // Type: KeyPath<MyEnum, (a: Int, b: String)?>

// Different
MyEnum.namedValues  // Type: (Int, String) -> MyEnum
MyEnum.namedTuple   // Type: ((a: Int, b: String)) -> MyEnum

QUESTION 1

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.

Never can be dealt with quite consistently:

\MyEnum.never       // Type: KeyPath<MyEnum, Never?>
e[keyPath: \.never] // Type: Never? (always nil in practice)

The empty case is difficult. It has no payload. Should we treat it like the void case?

\MyEnum.empty       // Type: KeyPath<MyEnum, Void?>
\MyEnum.void        // Type: KeyPath<MyEnum, Void?>

Or with a bool?

\MyEnum.empty       // Type: KeyPath<MyEnum, Bool>

Or with a compiler error?

// 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:

// \MyEnum.namedValue    // Impossible Type: KeyPath<MyEnum, (a: Int)?>
\MyEnum.namedValue       // Type: KeyPath<MyEnum, Int?>
e[keyPath: \.namedValue] // Type: Int?

It is time for the second desire: composed key paths.

@Joe_Groff, whose opinion matters, wrote:

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.

DESIRE 2

Those key paths should be expressible:

enum MyEnum {
    ...
    case values(Int, String)
    case tuple((Int, String))
    case namedValues(a: Int, b: String)
    case namedTuple((a: Int, b: String))
}

\MyEnum.values?.0        // Type: KeyPath<MyEnum, Int?>
\MyEnum.values?.1        // Type: KeyPath<MyEnum, String?>

\MyEnum.tuple?.0         // Type: KeyPath<MyEnum, Int?>
\MyEnum.tuple?.1         // Type: KeyPath<MyEnum, String?>

\MyEnum.namedValues?.a   // Type: KeyPath<MyEnum, Int?>
\MyEnum.namedValues?.b   // Type: KeyPath<MyEnum, String?>
\MyEnum.namedValues?.0   // Type: KeyPath<MyEnum, Int?>
\MyEnum.namedValues?.1   // Type: KeyPath<MyEnum, String?>

\MyEnum.namedTuple?.a    // Type: KeyPath<MyEnum, Int?>
\MyEnum.namedTuple?.b    // Type: KeyPath<MyEnum, String?>
\MyEnum.namedTuple?.0    // Type: KeyPath<MyEnum, Int?>
\MyEnum.namedTuple?.1    // Type: KeyPath<MyEnum, String?>

Fortunately, they are all consistent with the types of the basic key paths:

\MyEnum.values      // Type: KeyPath<MyEnum, (Int, String)?>
\MyEnum.tuple       // Type: KeyPath<MyEnum, (Int, String)?>
\MyEnum.namedValues // Type: KeyPath<MyEnum, (a: Int, b: String)?>
\MyEnum.namedTuple  // Type: KeyPath<MyEnum, (a: Int, b: String)?>

Now let's get wild.

DESIRE 3

Payloads that contain a single named value profit from the same support for composed key paths as payloads made of several named values

enum MyEnum {
    ...
    case namedValue(a: Int)
}

\MyEnum.namedValue?.a    // Type: KeyPath<MyEnum, Int?>
\MyEnum.namedValue?.0    // Type: KeyPath<MyEnum, Int?>

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.

Thanks for reading!

8 Likes