Extract Payload for enum cases having associated value

The namespace pollution you have in here is that this will add as many properties as there is cases in your enum... directly in your enum.

So you can't also have

enum Activity {
    case eat(foodName: String)
    case walk(origin: Place, destination: Place)
    case sleep(duration: TimeInterval)

    func sleep() -> Bool { return true }
    var walk: Double { if case let .walk(origin, destination) = self { return distance(from: origin, to: destination) } else { return 0 } }
    func execute(at: Date) { ... }
    var activityDuration: TimeInterval { ... }
}

Because then the generated code to access the payload will add 3 new computed properties (eat, walk, sleep) to access the respective payload, which will conflict with the existing user-provided properties or functions on the enum, and will also not be nicely namespaced but instead will be merged and mixed with the rest of the enum properties and functions, instead of being clearly separated in a dedicated namespace. While anEnumValue.as.caseName clearly namespace all the payload-accessing properties under a nice namespace without polluting the rest of the properties defined directly on the enum.

3 Likes

I've played with the OP code in a playground, focusing on the initially pitched idea: payload extraction.

First of all, it works well, and does what it says.

If the reliance on Mirror is off-putting to some people, it is an implementation detail that can be addressed in a compiler-blessed implementation.

Cases without any payload are not handled by the (AssociatedValue) -> Self) trick. I don't know if this hole is a problem or not.

One bug:

enum MyEnum: CaseAccessible {
    case one(Int)
    case two(Int, String)
}

// 2 (unexpected)
MyEnum.two(2, "foo").associatedValue() as Int?

Oh yes I missed it in the first post. It's good to know that the pitch is already living, it means that you have already tried it against real-life problems.

Just so I understand, the only difference you're proposing here as compared to my suggestion is to only have the equivalent of my as (and have it renamed payload instead of as), and not have is at all, right? Or am I missing another difference that you wanted to highlight?

I'm ok with dropping is (it was just a bonus in my Sourcery template) and renaming as. It's just as I thought those names had the advantages of being short while still descriptive, making it not much longer than if we accessed the property directly on the enum without that namespace since .as is only 3 chars... but also because I loved the nice parallel with is? and as?, that would add a nice consistency to the language. But as often, names can be bike shed :wink:

Hey @adtrevor, this solution I pitched allows you to cmd+click on the case and you will jump to the case.
It allows you to option+click on the case and you can get documentation for the case, along with its full definition.
These are features you loose with synthesized variables. It's a matter of context other than namespace pollution.

Even by defining nested structs to provide the kind of ergonomics you are proposing, the final result would be a bunch of structs and properties that the coder never asked for.
There is too much compiler magic going on.

we would need a simple "match" function that returns a boolean for no payload enum cases. If there is no payload, the compiler is going to stop you from trying to extract a payload. I got it for free, but this is how I wanted it to work. :D

Nice, I'll fix it, even if I think I agree on removing the pure associatedValue() function.

I'm sorry, I haven't found the time to read the whole thread, so sorry if it has been answered before, but could you sum up the features that we'd lose by going the synthesised variables?

I also find it borderline dangerous, because the type of the associated value does not convey any meaning:

enum MyResult {
    case success
    case recovered(from: Error)
    case failure(Error)
}

if let error = result.associatedValue() as Error? {
    // Play dice
}

EDIT: the real problem happens when the enum has unique payloads:

enum MyResult {
    case success
    case failure(Error)
}

// Looks legit
if let error = result.associatedValue() as Error? {
    ...
}

But then you add cases...

enum MyResult {
    case success
    // New! Break some code!
    case recovered(from: Error)
    case failure(Error)
}

@AliSoftware I'll collage a couple of quotes for you

True. I agree. it's dangerous. removing it from the pitch.

1 Like

Glad you removed it. Shifting from -1 to neutral.

Lol

Thanks a lot for taking the time to gather those quotes!

As I previously said, even if I'm not sure of the advantages of having an opt-in via CaseAccessible (as opposed of auto-conforming all enums to have the behaviour available on all enums for free), but I'm ok with having that CaseAccessible protocol for opt-in the feature if people feel it's better to have opt-in than having the feature unconditionally. So no problem there, and we could then make it so my alternative solution (which would give Foo.Bar.Baz.as.caseName return the Int? associated value) would only work on enums conforming to that protocol (and for which the compiler would thus have synthesised the accessor exactly at it already does for CaseIterable)

What I'm struggling about in your suggested solution though, is how we guarantee type-safety at the API level, while still restricting it to only work on the enum case names? And how could we allow short-hand syntax in that context?

IOW, I probably missed something, but if the signature is

associatedValue<AssociatedValue>(mathing pattern: (AssociatedValue) -> Self) -> AssociatedValue?`

Then:

  1. Since there's no enforcement to have AssociatedValue generic parameter be constrained to something specific, we can't use the .caseName short syntax. Since we already know that enumCase is of type Foo.Bar.Baz because we declared that variable with that type, why would I have to also specify the long name for the case? Once you declare let enumCase = Foo.Bar.Baz.caseName(parameter: 42) you already know it's a deep-nested enum yet you're still forced to use enumCase.associatedValue(matching: Foo.Bar.Baz.caseName) and can't use enumCase.associatedValue(matching: .caseName) can you?
  2. I can also provide as parameter any function that takes whatever and returns an instance of the enum. Including if I have func make(value: Int) -> Foo.Bar.Baz { return .caseName(value * 2) }, the compiler won't prevent me from writing enumCase.associatedValue(matching: make) then, which seems strange, right?

otoh, I like that using subscripts allows mutation :+1: which my solution does not allow (though I never had a use case for associated value mutation like that in practice, but if people feel it's needed then it's indeed a nice to have)

Technically, just like CaseIterable, anything can be CaseAccessible. Compiler will give it for free just for enums.

Just with mirroring, currently it's not possible. You'll have to specify the full "path", and like I described before it's something I kinda like. Maybe with some help from the compiler we can get away from it, but presently at the pitch phase I don't know it for sure.

True, the compiler won't stop you just like it won't stop you from going out of bounds when you query an array element >= count. With pure Mirroring proposal, I don't have a solution to this.

Yeah which is why I'm a bit reluctant to depend on Mirror on that, which is unprecedented as far as I know in terms of pitches and proposals, and one of the reason I'd prefer to have it as a full language feature and/or compiler-generated feature :wink:

I think at that point the feature is so interesting and so many people are interested by it that we better make it ironclad and type-safe than rely on something that allows side-effects or misuses. Which is also why, I think, multiple people are reluctant on approving an implementation based on Mirror.

I think having it as a full language feature, that would allow enumCase[associatedValue: .caseName] (and also enumCase[associatedValue: Foo.Bar.Baz.caseName] for those like you who'd prefer long syntax over short-hand syntax) or similar without relying on Mirror, and would not allow (= compiler error) enumCase.associatedValue(matching: make) – ensuring consistency of the usage of the feature – would be more beneficial and make the pitch adoption a bit stronger.

If this can be enforced by the compiler that only names of cases corresponding to the enum type could be passed as parameter, then it would solve a great deal of risks imho, allow short-hand syntax and avoid misuses like with make, while maintaining the benefits of your suggestion (I like the subscript syntax in your suggestion more and more... as long as it can't be misused like it can now and doesn't rely on the brittle Mirror :wink: ). So basically, keep your nice API but make it generated+controlled by the compiler to ensure type-safety, ability to short-hand, and avoid misuse. wdyt?

2 Likes

I'm not proposing to do it with mirror. This is an implementation detail. I built this proposal using Mirror so that you could try it in your playground. What I'm advertising is the approach, not the implementation.

1 Like

So, if I understand correctly, the issue is more about avoiding conflicts, right?
Let's say we have an "extractor operator" (here, I use ?) that, when placed after a case name, causes it's payload to be extracted, then even if we have a foo case and a foo associated property, calling myVariable.foo? would unambiguously refer to the case, not the property.

I'm not saying this is ideal, but I wonder if, starting from such an approach, we could find a better way of spelling extraction?

That's totally doable, and I like the postfix idea. What do you suggest as postfix operator?

? is taken

I don't really know... maybe something foo~?caseName, foo.?caseName, foo?~caseName or foo>?caseName...

But I think others would have better ideas.

Before this, the true question is to know if that's a correct approach or not, which I'm not sure at all! It's fun to imagine new syntaxes but quite hard to know what's best to do.

For now the subscript is not that bad

Re-reading your post, I actually like the general principle although I'm a little worried that it's not immediately explicit that an optional is returned. I don't like much default: since it seems to me that ?? would also do the job when needed.
Also is TypeName.case absolutely necessary? It would seem simpler if we could just use .case.

On the other hand, I don't appreciate the update function (but I'm 100% in favour of enabling mutating the associated values), it looks too much like a hack rather than a well integrated language feature.