Extract Payload for enum cases having associated value

True, Because enum cases would now do double-duty: when used without arguments they refer to their family (eg: .baz) and when used with arguments, they more-or-less refer to an instance (eg: baz(123.0) ).

If we make .a() an optional, how big a problem is this? To provide the same functionality with Swift at present requires a lot of code (eg: a second enum, custom getter, then using switch statements and "if let case" nonsense).

I think this might be a problem in theory, but a non-issue in practice. Even now, an enum with a payload in Swift is already basically a type. Whether we use existing Swift enums, or the paradigm I like, the programmer needs to know about the specific case, and the type(s) of its payload.

I should also point out, that these problems are those that occur using my hacky enum-like structs in Swift, as it exists now. Even so, I find them minor — if they were built right into Swift, the compiler could, for example, type check the second example live. Maybe even the first example could be solved with some sort of magic.

To make things less abstract, in practice, we're looking at code, at the call-site, like:

// if enum value is a .magnitude, add it to an array of Doubles
if measurement == .magnitude { 

    doubles.append( measurement() ?? 0.0 )

}

or

// filter an array of enum cases to contain only those that are .magnitude, regardless of their custom payload
let magnitudes = readings.filter{ $0 == .magnitude }

I'm not sure if those examples do it justice. I currently use this for getting and setting class parameters, and, at the call-site, it's really nice. Declaring the things, even with a protocol doing most of the work, is still ugly, because it requires a lot of duplicated code (each payload case needs both a var and a func).

let a: MyEnum = .bar(123.0)
let c: MyEnum = .bar(456.0)

a == c   // true (same case)
a === c  // false (different payloads)

Right now, enumeration equality considers the case and payload. So your idea above, making case-only equality the default and bumping deep equality to a side operator, is 100% source breaking right off the bat! And it breaks Equatable. What about types that don't (want to) have equality operators defined? And I think that new users would rarely want case-only comparisons, let alone them out-prioritizing conventional comparisons.

Case-only comparisons should use a function that extracts the tag:

assert(case(of: a) == case(of: c))

Or maybe a "#caseOf" built-in function. It would be a complement of the

a is case .bar(_:)

idea I've had. We would need to add a standard library type to represent enumeration case tags. Could key paths be extended for this?

1 Like

I think it's a little bit of each, but maybe the third weighs most heavily. A few of us are very interested in fleshing out a proposal, but we'd need a compiler engineer to get an implementation. The reflection mechanism in EnumKit inspired the reflection mechanism in swift-case-paths, and folks have joked that "swift-case-paths" could suffice as the "implementation" of an evolution proposal, but without real compiler support I think it'd be a tough sell.

These case paths have already proven to be very useful in "real world," production applications:

  • They are used as a tool for composing the fundamental unit of applications built using swift-composable-architecture. They give developers the ability to break down large, complex applications into smaller ones and then glue them all back together using key paths and case paths.

  • They can be used to derive mutable SwiftUI.Bindings from enum state. Currently, enum state is so cumbersome in SwiftUI that you are typically led to model your app's domain in ways that introduce invalid states and bugs.

A few unanswered "future direction" questions:

  • What does "dynamic member lookup" look like for case paths?
  • How should "key paths" and "case paths" compose? They form their own structure, which models optional chaining:
    // optional get (User) -> String?
    user.location?.name
    // required set (String) -> User
    user.location?.name = "LA"
    // 🛑 error: 'nil' cannot be assigned to type 'String'
    user.location?.name = nil
    
    This means we must also consider a third "optional path" of sorts and how they would fit into the "hierarchy" of paths is uncertain.
5 Likes

If this is an issue, it can be handled as we do elsewhere, by making it dependent on conforming to a protocol, like Equatable or Hashable

Re: beginner expectations, note that the changes I propose only impact payload enums. I doubt beginners are able use those at all.

Oops. Yes, that's a dose of reality. I suppose if I continue to advocate for my paradigm (wish I had a less annoying word than "paradigm", btw), I should switch to proposing it as an additional kind of datatype or feature, that is like, but not a replacement for, existing Swift enums. And that's unlikely to happen, because Swift is complicated enough, as it is :(

Okay, I'll stop derailing the thread now, and focus my efforts on coming up with a proposal that doesn't aim to replace enums, but rather adds an additional protocol and magic, along the lines of RawRepresentable.

One thing that I think may be doable is a shorthand for extracting a payload as an optional if the enum has some given case. All the ways I can think to write it are kind of overly verbose:

If-case:

let payloadIfSomeCase: PayloadType?
if case .someCase(let payload) = enumValue {
  payloadIfSomeCase = payload
} else {
  payloadIfSomeCase = nil
}

Switch:

let payloadIfSomeCase: PayloadType?
switch enumValue {
case .someCase(let payload): payloadIfSomeCase = payload
default: payloadIfSomeCase = nil
}

In both of these situations, you need to write the payload type out manually. The payload extraction happens inside a new scope, so to hoist it out in to a local variable, you need to declare that variable before-hand, meaning you don't get type inference. You also need to either handle the else condition or make the variable mutable rather and give it a default value of nil.

Perhaps something like:

case? .someCase(let payloadIfSomeCase) = enumValue
payloadIfSomeCase?.someFunction()

Which is almost exactly the same as the line in the switch statement above, mixed with tuple destructuring:

(let x, var y) = someFunctionReturningATuple()
x.someFunction()

Something that is small and targeted like this would, IMO, have a higher chance of being accepted than some of the more creative solutions I've seen floating around, which would often involve lots of code generation, reflection, or other magic abstractions.

We could use something like key paths to represent lenses/prisms.

enum MyEnum {
    case first
    case second(Int, Double)
}

let a = MyEnum.second(5, -6.5)
let b = a[keyPath: \.second]   // should be a "(Int, Double)?"
let c = a[keyPath: \.second.1] // should be a "Double?"
let d = a[keyPath: \.first]    // should be a "Void?", which will be NIL here
8 Likes

I came here to propose exactly this. It's short, natural and familiar.

So far (correct me if I'm wrong on this) key paths don't have any special syntax exclusive to them. When you can write a[keyPath: \.x] you can also write a.x, no matter what x actually is (I guess you have to drop the dot for subscripts, but you get my point.

I don't think it would be a good idea to change this and have special features that are only available via key paths, so imo it would be necessary to have the same variables available on the enum value itself.

This syntax also would need to be extended to handle enums where cases are distinguished only by parameter labels:

enum Lol {
  case foo(s: String)
  case foo(i: Int)
}

let a = Lol.foo(i: 42)
let b = a[keyPath: \.foo] // what is this?
1 Like

x can't be methods, applied or unapplied:

struct A {
    func foo(i: Int) { }
}

\A.foo // Error
\A.foo(i: 3) // Error

Yes. I'm not saying "a.x works" implies "a[keyPath: \.x] works", just the reverse.

1 Like

It would be akin to a key path. The "\MyType.myCase" format looks just a nice as it does for actual key paths. We can call the subscript "case:" instead. Hopefully, we can make the parser accept "\MyType.myCase(myFirstPayload: mySecondPayload:)" syntax.

I personally think this is an interesting syntax to extract enum payloads.

To correctly infer the right payload-tuple type, the enum case needs to be passed in some way. You suggested to use myEnumInstance[keyPath: \.myCase]! to access the associated values, but that would require your Enum to conform to a new protocol for API stability.
On the other hand, as? case or as! case would work without new requirements.

I'm interested in having analogies between enums and optionals (which are enums):

let a: Int? = 3

// unwrap value in an optional
if let a = a { ... }
if let a = a as? Int { ... }
// force unwrap value in an optional
let b = a!
let b = a as! Int

// unwrap values in an enum
if case let .some(x) = a { ... }
if let x = a as? case .some { ... } // new syntax, x is Int
// force unwrap values in an enum
let x = a as! case .some            // new syntax, x is Int

Enum force unwrapping would then be chainable as optional force unwrapping:

enum Enum1 { case .foo(Int, String) }
enum Enum2 { case .bar(Enum1) }

let a: Enum2 = .bar(.foo(3, "three") }

let (num, string) = (a as! case .bar) as! case .foo // num is Int, string is String
Terms of Service

Privacy Policy

Cookie Policy