Extract Payload for enum cases having associated value

I'm afraid the goal isn't easy to reach. Let me try to explain what I find difficult and really odd considering what I know of the language:

I don't know what this .bar is, actually. I can't make it fit in the bestiary of "things" Swift let us use in our programs, as defined by the grammar reference.

I mean that it looks to me that your goal requires the invention of a whole new kind of "thing" so that it can be made workable. Not only a "thing" that is internal to the compiler. A "thing" that exists in userland:

// Perform event[case: .bar] in two steps:
let myCase = ... // what do we write here? What is the type of this thing?
event[case: myCase]
if case .bar = enumCase { }

what is on the left side of the equal? I believe that this is where enum fails. The impossibility to give a name to the left side of this expression. we call it pattern, but there is nor formal representation of what an enum pattern is.

This is part of a statement. Statements and expressions occupy different spaces. There's no way to capture the .bar of a pattern match and use it in a later pattern match. However, when you can pass a .bar to your subscript solution, this is an expression, so it must refer to some kind of object/value.

1 Like

You don't really care, because you can't split this snippet. It's an unbreakable "atom of syntax", as far as we are concerned.

So we don't really have to wonder what is this .bar, just in case some user would want to extract it from the pattern matching snippet and ask "what is the type of the variable I can store this .bar in?".

This here is the main point that tells me to not approve this pitch, even if it presents some clever workaround to the huge holes we have today in the language for what concerns enums.

I really appreciated the effort that @GLanza put in adding mutability, this is 100% something I would add to an official proposal on this. Also, I'd admit that if we don't get a different solution that I'd consider better, I'm fine with this, it's definitely better than nothing.

But keypaths are, to me, the way. As an heavy user of keypaths, one that writes lots of wonderful generic code based on them, I can say that they are one of my favorite Swift features, and they're becoming more and more relevant to the language (see for example the glorious SE-0252).

Key paths are automatically defined for any property, computed or not, this is not an opt-in behavior, and shouldn't be. The only thing missing to enums are the boilerplate-y computed properties for cases, so everything would be easily solved if those where generated, and I still don't see any problem in having them in the namespace, because they would be simply considered "features" of enum instances.

The approach taken in the pitch, to refer to a case with its constructor, unfortunately makes it a little too cumbersome to use, because we'd always forced to refer the full constructor: in Swift the .something syntax is used if and only if the context expects an instance of a certain type, and .something is a static function on that type (thus, also an enum case constructor).

But the solution is super simple and right at hand: generate keypaths for enums, and use keypaths instead of the case constructor. This approach would be perfectly in line with the current Swift state of art and philosophy.

The difference here is that if case ... is a statement, not an expression. Swift clearly separates evaluated expressions (something that you would pass as a parameter to a function, for example) to statements, that you cannot pass around, and are not evaluated.

For example:

enum Foo {
  case bar(Int)
}

let foo: Foo = .bar(42)

switch foo {
case .bar:
  break
}

let doesntCompile: Foo = .bar

The .bar in case .bar is part of a statement and doesn't follow the same rules as expression, it doesn't need to be "something" itself.

The .bar in let doesntCompile: Foo = .bar won't compile because we need an expression that evaluates to "something" there. Taken as an expression, .bar should be a (Int) -> Foo function, but it's not defined on the (Int) -> Foo type, in fact:

let stillDoesntCompile: (Int) -> Foo = .bar

causes the compilation error type '(Int) -> Foo' has no member 'bar', because .bar is defined on the type Foo. There's no way out here.

1 Like

I want to have a less direct conclusion, and expose some its intermediate steps.

The code below can't compile, because nothing says that the .bar "thing" has to come from Foo:

let myCase = .bar // WAT?
enum[case: myCase]

What can we change to make it compile?

What about an explicit type information, which would help type inference? The code below doesn't compile today, but it could be made valid in a future compiler version. All the required information is there:

let myCase: (Int) -> Foo = .bar // Could be made to compile one day.
enum[case: myCase]

The type (Int) -> Foo is the only possible type that we can put at this place today, as long as .bar, an Implicit Member Expression remains defined as:

An "implicit member expression" is an abbreviated way to access a member of a type, such as an enumeration case or a type method, in a context where type inference can determine the implied type.

(Emphasis mine). The only known member named bar that we know is Foo.bar, typed (Int) -> Foo. It is the only one we currently have.

Can we invent a new bar member somewhere else? Yes we can, many above have tried, with the synthesis of various derived types from the base enum (see messages above). But those are other pitches.

So, (Int) -> Foo. And now we have a real problem. It has already been stated above, I don't invent it. The real problem is that (Int) -> Foo falls short in terms of type-safety. Would the [case:] subscript accepts any such function, and not only cases from the Foo enum, the proposal would sound too dangerous for many.

So... I think the the suit is too tightly cut. We don't have enough freedom. We actually can't really "make it compile somehow", because we face contradictions or undesired side effects.

I hope it would never be the case. While type inference is often fine, for such case, you can't know from where the .bar member come from without browsing the whole Module code.

The module can perfectly define a method like this:

enum Foo  {
   case misc
}

struct Bar {
   func bar(a: Int) -> Foo {}
}

let myCase: (Int) -> Foo = .bar

To know that, you must parse the whole module, which is not something anybody want, neither for compiler performance, nor for human readability of the code.

It could be made possible for this pitch precisely, if it were to be accepted and implemented, without having to browse the whole Module.

This is why I have looked for other, harder, issues.

I'm not sure it could be made to compile. I might be mistaken, but Foo.bar is not a "member" of the type (Int) -> Foo, but an "instance" of it. To make the thing compile an actual member should be defined, for example I think compilation would be successful if the we could write or synthesize the following code:

/// pseudoswift

extension (Int) -> Foo {
  static var bar: (Int) -> Foo {
    return Foo.bar
  }
}
1 Like

@ExFalsoQuodlibet and @Jean-Daniel, you are both right, and somewhat wrong. You are right because the current compiler can't turn .bar into (Int) -> Foo. But compiler can change! That's the whole point of Swift Evolution!

We can't answer @GLanza "this is impossible" when something actually is possible with a little effort. Bringing to the light what are the necessities and the contingencies of a proposal is the whole point of our conversations here.

This is why I consider the argument "type inference can't do this" as a weak one. I find other issues that appear, after the type inference step has been solved, much more concerning.

let myCase: (Int) -> Foo = .bar

.bar is a synthesized static func of Foo. Could the compiler check this?

Even if the compiler would check this, how could it disambiguate cases with the same payload type, or avoid users from feeding the [case:] subscript with any kind of (Int) -> Foo function? How can (Int) -> Foo alone contain enough information?

I know you have made a clever use of Mirror, which helps solving the case disambiguation. But how robust is this solution?

I think this is where the key path crowd scores a very strong point: KeyPath is the only current type of the language that is designed to contain all needed information about its target.

with (Int) -> Foo you actually have a prototype of the case you are looking for. That's how I used it in my solution with Mirror. You literally have all the informations you need about the wanted case.

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

Terms of Service

Privacy Policy

Cookie Policy