Extract Payload for enum cases having associated value

You are mistaken.

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.

1 Like

Sorry, I was focused on the short syntax enumCase[case: .bar]. You are right, your library handles this case.

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.

1 Like

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:

  1. An operation that converts Reducer<LocalState, LocalAction> to Reducer<GlobalState, GlobalAction>
  2. 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).

5 Likes

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:

\Struct.property  // {{Reference,}Writable,}KeyPath
\Enum.case(name:) // EnumKeyPath
\Value.method()   // MethodKeyPath
7 Likes

Here is the same result with EnumKit

import Combine
import SwiftUI

struct AppState {
  var count = 0
  var favoritePrimes: [Int] = []
  var loggedInUser: User? = nil
  var activityFeed: [Activity] = []

  struct Activity {
    let timestamp: Date
    let type: ActivityType

    enum ActivityType: CaseAccessible {
      case addedFavoritePrime(Int)
      case removedFavoritePrime(Int)
    }
  }

  struct User {
    let id: Int
    let name: String
    let bio: String
  }
}

enum CounterAction {
  case decrTapped
  case incrTapped
}
enum PrimeModalAction {
  case saveFavoritePrimeTapped
  case removeFavoritePrimeTapped
}
enum FavoritePrimesAction: CaseAccessible {
  case deleteFavoritePrimes(IndexSet)
}
enum AppAction: CaseAccessible {
  case counter(CounterAction)
  case primeModal(PrimeModalAction)
  case favoritePrimes(FavoritePrimesAction)
}

let someAction = AppAction.counter(.incrTapped)
someAction[case: AppAction.counter]
someAction[case: AppAction.favoritePrimes]

//\AppAction.counter
// WritableKeyPath<AppAction, CounterAction?>


// (A) -> A
// (inout A) -> Void

// (A, B) -> (A, C)
// (inout A, B) -> C

// (Value, Action) -> Value
// (inout Value, Action) -> Void

//[1, 2, 3].reduce(into: <#T##Result#>, <#T##updateAccumulatingResult: (inout Result, Int) throws -> ()##(inout Result, Int) throws -> ()#>)

func counterReducer(state: inout Int, action: CounterAction) {
  switch action {
  case .decrTapped:
    state -= 1

  case .incrTapped:
    state += 1
  }
}

func primeModalReducer(state: inout AppState, action: PrimeModalAction) -> Void {
  switch action {
  case .removeFavoritePrimeTapped:
    state.favoritePrimes.removeAll(where: { $0 == state.count })
    state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.count)))

  case .saveFavoritePrimeTapped:
    state.favoritePrimes.append(state.count)
    state.activityFeed.append(.init(timestamp: Date(), type: .addedFavoritePrime(state.count)))
  }
}

struct FavoritePrimesState {
  var favoritePrimes: [Int]
  var activityFeed: [AppState.Activity]
}

func favoritePrimesReducer(state: inout FavoritePrimesState, action: FavoritePrimesAction) -> Void {
  switch action {
  case let .deleteFavoritePrimes(indexSet):
    for index in indexSet {
      state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.favoritePrimes[index])))
      state.favoritePrimes.remove(at: index)
    }
  }
}

//func appReducer(state: inout AppState, action: AppAction) {
//  switch action {
//  }
//}

func combine<Value, Action>(
  _ reducers: (inout Value, Action) -> Void...
) -> (inout Value, Action) -> Void {
  return { value, action in
    for reducer in reducers {
      reducer(&value, action)
    }
  }
}
final class Store<Value, Action>: ObservableObject {
  let reducer: (inout Value, Action) -> Void
  @Published private(set) var value: Value

  init(initialValue: Value, reducer: @escaping (inout Value, Action) -> Void) {
    self.reducer = reducer
    self.value = initialValue
  }

  func send(_ action: Action) {
    self.reducer(&self.value, action)
  }
}

func pullback<LocalValue, GlobalValue, LocalAction, GlobalAction: CaseAccessible>(
  _ reducer: @escaping (inout LocalValue, LocalAction) -> Void,
  value: WritableKeyPath<GlobalValue, LocalValue>,
  action: @escaping (LocalAction) -> GlobalAction
) -> (inout GlobalValue, GlobalAction) -> Void {
  return { globalValue, globalAction in
    guard let localAction = globalAction[case: action] else { return }
    reducer(&globalValue[keyPath: value], localAction)
  }
}


func pullback<LocalValue, GlobalValue, LocalAction, GlobalAction>(
  _ reducer: @escaping (inout LocalValue, LocalAction) -> Void,
  value: WritableKeyPath<GlobalValue, LocalValue>,
  action: WritableKeyPath<GlobalAction, LocalAction?>
) -> (inout GlobalValue, GlobalAction) -> Void {
  return { globalValue, globalAction in
    guard let localAction = globalAction[keyPath: action] else { return }
    reducer(&globalValue[keyPath: value], localAction)
  }
}

extension AppState {
  var favoritePrimesState: FavoritePrimesState {
    get {
      FavoritePrimesState(
        favoritePrimes: self.favoritePrimes,
        activityFeed: self.activityFeed
      )
    }
    set {
      self.favoritePrimes = newValue.favoritePrimes
      self.activityFeed = newValue.activityFeed
    }
  }
}

struct _KeyPath<Root, Value> {
  let get: (Root) -> Value
  let set: (inout Root, Value) -> Void
}

AppAction.counter(CounterAction.incrTapped)

let action = AppAction.favoritePrimes(.deleteFavoritePrimes([1]))
let favoritePrimes: FavoritePrimesAction?
switch action {
case let .favoritePrimes(action):
  favoritePrimes = action
default:
  favoritePrimes = nil
}


struct EnumKeyPath<Root, Value> {
  let embed: (Value) -> Root
  let extract: (Root) -> Value?
}
// \AppAction.counter // EnumKeyPath<AppAction, CounterAction>

let _appReducer: (inout AppState, AppAction) -> Void = combine(
  pullback(counterReducer, value: \.count, action: AppAction.counter),
  pullback(primeModalReducer, value: \.self, action: AppAction.primeModal),
  pullback(favoritePrimesReducer, value: \.favoritePrimesState, action: AppAction.favoritePrimes)
)
let appReducer = pullback(_appReducer, value: \.self, action: \.self)
  //combine(combine(counterReducer, primeModalReducer), favoritePrimesReducer)


// [1, 2, 3].reduce(<#T##initialResult: Result##Result#>, <#T##nextPartialResult: (Result, Int) throws -> Result##(Result, Int) throws -> Result#>)

var state = AppState()
//appReducer(state: &state, action: .incrTapped)
//appReducer(state: &state, action: .decrTapped)
//print(
//  counterReducer(
//    state: counterReducer(state: state, action: .incrTapped),
//    action: .decrTapped
//  )
//)
//counterReducer(state: state, action: .decrTapped)

// Store<AppState>

struct PrimeAlert: Identifiable {
  let prime: Int
  var id: Int { self.prime }
}

struct CounterView: View {
  @ObservedObject var store: Store<AppState, AppAction>
  @State var isPrimeModalShown = false
  @State var alertNthPrime: PrimeAlert?
  @State var isNthPrimeButtonDisabled = false

  var body: some View {
    VStack {
      HStack {
        Button("-") { self.store.send(.counter(.decrTapped)) }
        Text("\(self.store.value.count)")
        Button("+") { self.store.send(.counter(.incrTapped)) }
      }
      Button("Is this prime?") { self.isPrimeModalShown = true }
      Button(
        "What is the \(ordinal(self.store.value.count)) prime?",
        action: self.nthPrimeButtonAction
      )
      .disabled(self.isNthPrimeButtonDisabled)
    }
    .font(.title)
    .navigationBarTitle("Counter demo")
    .sheet(isPresented: self.$isPrimeModalShown) {
      IsPrimeModalView(store: self.store)
    }
    .alert(item: self.$alertNthPrime) { alert in
      Alert(
        title: Text("The \(ordinal(self.store.value.count)) prime is \(alert.prime)"),
        dismissButton: .default(Text("Ok"))
      )
    }
  }

  func nthPrimeButtonAction() {
    self.isNthPrimeButtonDisabled = true
    nthPrime(self.store.value.count) { prime in
      self.alertNthPrime = prime.map(PrimeAlert.init(prime:))
      self.isNthPrimeButtonDisabled = false
    }
  }
}

struct IsPrimeModalView: View {
  @ObservedObject var store: Store<AppState, AppAction>

  var body: some View {
    VStack {
      if isPrime(self.store.value.count) {
        Text("\(self.store.value.count) is prime 🎉")
        if self.store.value.favoritePrimes.contains(self.store.value.count) {
          Button("Remove from favorite primes") {
            self.store.send(.primeModal(.removeFavoritePrimeTapped))
          }
        } else {
          Button("Save to favorite primes") {
            self.store.send(.primeModal(.saveFavoritePrimeTapped))
          }
        }
      } else {
        Text("\(self.store.value.count) is not prime :(")
      }
    }
  }
}

struct FavoritePrimesView: View {
  @ObservedObject var store: Store<AppState, AppAction>

  var body: some View {
    List {
      ForEach(self.store.value.favoritePrimes, id: \.self) { prime in
        Text("\(prime)")
      }
      .onDelete { indexSet in
        self.store.send(.favoritePrimes(.deleteFavoritePrimes(indexSet)))
      }
    }
    .navigationBarTitle("Favorite Primes")
  }
}

struct ContentView: View {
  @ObservedObject var store: Store<AppState, AppAction>

  var body: some View {
    NavigationView {
      List {
        NavigationLink(
          "Counter demo",
          destination: CounterView(store: self.store)
        )
        NavigationLink(
          "Favorite primes",
          destination: FavoritePrimesView(store: self.store)
        )
      }
      .navigationBarTitle("State management")
    }
  }
}

// import Overture

import PlaygroundSupport
PlaygroundPage.current.liveView = UIHostingController(
  rootView: ContentView(
    store: Store(initialValue: AppState(), reducer: appReducer)
  )
)

I've pitched a different solution here DiscriminatedUnion Protocol

1 Like

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

4 Likes

Alright, let's try to put together an EnumKeyPath proposal. I'm cool with it, as long as we don't generate properties.

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.

Are you open to allowing a way to opt-in to generating properties?

1 Like

As opt in, yes. Generated properties are very invasive. I believe that users should be able to choose.

2 Likes

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.

Resistance is futile.

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.

:rofl:

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.

  1. Do we want to access associated values with better ergonomics than switch and pattern matching?
  2. 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.

This is my position about the matter, today.

No, @GLanza, no. It's not because they don't fit today that they can never fit. You can for example, use the information provided by @Jumhyn above.

In order to deduce for good that a type does not fit, you have to show that its apis and behavior exhibit an inconsistency or an unpluggable hole.

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

Here is an example:

extension MyEnum {
    var value: Int? {
         self[keyPath: \.value]
    }
}

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
1 Like