Extract Payload for enum cases having associated value

I agree that the compiler can change, of course ;)

But I see a problem with this particular change, because the current usage of the implicit member expression is unambiguous: it must be the member of the type required by the context, that is, either a static function/property that returns an instance of that type, or an enum case constructor, which is the basically the same thing as the first option. I currently don't think that changing this behavior is desirable.

But I agree that there are greater concerns about the pitch.

I guess it could, but it would have a different meaning than the current implicit member expression, and I don't think it's a good idea.

For example, if in magical future swift functions become nominal types, and we'll be able to add members to them (this is something that a huge chunk of the community wants to happen), that syntax would be ambiguous.

Also, if (Int) -> Foo was typealiased, like the following:

typealias FooProvider = (Int) -> Foo

let myCase: FooProvider = .bar 

I would expect .bar to be something that returns a FooProvider, and that I can call on FooProvider.

That's what makes keypaths great! And I think their usefulness is probably something that was glimpsed only by those that tried to use keypath and experiment with them in generic algorithms. I have spoken with a number of swift users that don't use keypaths at all, mostly because they don't understand them or know their existence, and that's fine: it's part of the "progressive disclosure" that makes the language great for newcomers. I think there's also plenty of users that don't understand what enums with associated types are for.

But, as this doesn't undermine the fact that enums are the correct way to represent a data structure with alternative options, keypaths are the correct way to represent the "connection" between a data structure and its members, at any level of nesting.

I think the point here is that you could use any function where you need an instance of (Int) -> Foo, not just Foo.bar.

1 Like

Except that it must be a signature of an enum case. With Mirror, you start with the enum type, and only match signatures of members of the type. If all you have is the type signature, there's nothing to tell you that it's from an enum, never mind which enum.

I just realized that instead of generating the computed properties for an enum, we could generate the keypaths, and use keypath member lookup to get the properties automatically :neutral_face:

1 Like

Yes. Maybe this thread is painful to read for our fellows here who are already convinced that key paths are the only viable solution to our "problem". Contemplating so many of us slowly crawling our way must be a tough game for their patience.

Maybe key paths are Swift's monads :-) I learned about Crockford's law today, it is funny :-)

I'd say that they are Swift's optics :smiley: which is another kind of obscure (but super useful!) FP topic.

2 Likes

Like below?

let result: Result<S, F> = ...
result.success // does not compile
result.failure // does not compile
let success = result[keypath: \.success]) // S?
let failure = result[keypath: \.failure]) // F?

This would be an interesting twist of Automatically derive properties for enum cases - #5 by stephencelis. No properties. Only key paths. But this is another pitch.

1 Like

The most obvious problem I see with this API is the fact that the pattern is a function, and therefore may, theoretically, have side effects, which in the current implementation may be evaluated multiple times.

If possible, I would prefer the pattern to be an instance of Self.Discriminator from:

protocol CaseDiscriminatable {
  associatedtype Discriminator: Hashable
  var case: Discriminator
}

With a compiler generated implementation for enums.

CaseAccessible would then inherit from CaseDiscriminatable.

Can you expand this idea with an example of what the compiler should generate?

For CaseDiscriminatable?

enum Foo: CaseDiscriminatable {
  case a
  case b(Int)
  case c(x: Int)
  case d(Int, String)
}

// Generates:
extension Foo {
  var case: Discriminator { 
    switch self { /*... */ } 
  }

  enum Discriminator: Hashable {
    case a
    case b
    // This would require multi-part (non-function) variable names,
    // which would be useful for closures as well.
    // but needn't be user definable for this feature.
    case c(x:)
    case d
  }
}

So the method signature would become

func associatedValue<AssociatedValue>(matching discriminator: Foo.Discriminator) -> AssociatedValue?

The only problem I see with this is that AssociatedValue can't be inferred, and it's left to the user to be specified

let value: String = myEnum.associatedValue(matching: .b) //It would compile
// but b does not have a String associated value

We move the type safety problem, but it's still there, this time in the return type.

1 Like

Just my two cents, but I think that @stephencelis's enum key paths is the most simple solution and consistent with current language design. Occam's razor suggests we just extend an already existing language feature rather than implement a new one.

I'd love to see a enum key paths proposal, I will preemptively +1 it. :smile:

9 Likes

I disagree on consistency. it doesn't happen anywhere in the language that the compiler has to generate computed properties for a data structure even if the developer didn't ask for it. It's super invasive. Maybe the closest thing we have to this are CaseIterable and Codable, yet they are protocols you can avoid to use and the compiler won't interfere with your design.

Yes @GLanza, but as I attempted to say above, we could have enum key paths without properties.

In this case, you could write:

let result: Result<String, Error> = ...
let success = result[keypath: \.success]) // String?

This is very similar to your goal:

The convergence looks obvious to me:

let success = result[keypath: \.success]) // String?
let success = result[case: .success])     // String?

Are you sure you are opposed to enum key paths? They achieve your goal, in the frame of the language. We don't have to invent "case values" that are difficult to define properly.

If the argument is "consistency" this is unprecedented within the language. There is no place I'm aware of in the language that key paths exist independently from properties.

I still believe this should be opt-in. If users want this feature they should be able to control the generation of these properties or keypaths only.

All right ! Just as opt-in as the conformance to your CaseAccessible protocol. The convergence is now complete. But in one case only we have full information and type safety.

Both forms use subscript access, and only differ in the parameter name. I can see an argument for using a parameter name that already exists in the language (keypath), rather than introducing a new one (case). On the other hand, I also see value in naming the parameter correctly, and I find your argument that cases are not really keypaths to be compelling.

3 Likes

How key paths would be generated?

enum Foo: CaseAccessible {
  case bar(Int)
  case bar(name: String)
  case baz(Int, String)
  case bla(name: String, lastName: String)
  case bla(name: String, age: Int)
}

let enumCase: Foo

enumCase[keyPath: \.bar] // ??? which one?
enumCase[keyPath: \.bar.name] // String?
enumCase[keyPath: \.baz] // (Int, String)?
enumCase[keyPath: \.bla] // ??? which one?
enumCase[keyPath: \.bla.name] // ??? which one?
enumCase[keyPath: \.bla.lastName] // String?
3 Likes

Unless I'm mistaken, your stated goal (event[case: .bar]) has the very same exact issue with cases that share names, @GLanza. And it does not even allow compound identifiers like .bar.name.

I guess we have to live with the consequences of SE-0155 Normalize Enum Case Representation:

[...] shared base name will be allowed:

enum SyntaxTree {
    case type(variables: [TypeVariable])
    case type(instantiated: [Type])
}

A solution could be that such enums can not be supported, or only partially supported. This is what a "Detailed Design" section of a proposal should have to describe precisely.

For the record, the original pitch from @stephencelis did explore the consequences of SE-0155. I'm sure it's worth a re-read.

Edit: Oh, and they also thought about the funny case of Never payload :slight_smile:

enum Bizarre {
    case normal(Never)
    case weird(String)
}

// Should we allow this?
let alice: Bizarre = ...
alice[keyPath: \.normal] // Or alice[case: .normal] - this is the same

Handling the shared base names situation seems like something that could fall out of an implementation of the "next steps" after SE-0111:

Where these compound names could also be used to refer to enum cases as, e.g. syntaxTree[keyPath: \.type(variables:)].

1 Like