Extract Payload for enum cases having associated value

The last 2 answers from @Tino synthesize why we shouldn't have synthesized properties:

I'll add:

  • to avoid overloading issues naming should be really weird and verbose. notRequestedWithShouldRequest
  • extensions of Collection, AnyPublisher, Observable, Signal would be impossible
extension Collection where Element: ???? { }

If you believe that having properties will cover more use cases, then we can agree to disagree. Generic function with protocol (similarly to the existent CaseIterable) is in my opinion the way to go, and it is what I'm pitching now. To propose something completely different is acceptable, but I believe it deserve another topic.

The objection is that this automatic synthesis pollutes the namespace: this is a valid point, but I'd weight that against the advantages, and evaluate the tradeoffs. As you noticed, my generated code tends to be verbose, so the likelihood of collision with some custom computed properties is very low.

I don't see a problem here, verbosity should be evaluated at usage site, for example:

values
  .compactMap {
    guard case let .notRequested(shouldRequest) = self else { return nil }
    return shouldRequest
  }

is much more clunky and less readable than:

values
  .compactMap {
    $0.notRequestedWithShouldRequest
  }

I wouldn't need them: instead of defining new methods that do the same thing as the basic ones, but in the special case of a CaseAccessible, I'd use (and I actually do) the standard ones where I call the generated properties in the closures.

It seems to me that basic methods + properties (thus, functions) is a more composable approach that any approach based on protocols.

But I am not proposing

guard case let .notRequested(shouldRequest) = self else { return nil }
    return shouldRequest

but this pitch is not for you specifically but for the swift community that has many other use cases. See the question by @zoul in the first topic. It's the example where an extension of Signal with CaseAccessible would solve, while your approach wouldn't.

My useCase similar to the one proposed by @zoul is covered by CaseAccessible by extending Observable, similar cases in Combina would be covered too. Plus, I'm not polluting the namespace and certainly

values
  .compactMap {
    $0.associatedValue(case: .notRequested)
  }

Or its equivalent with Collection Extension

values
  .map(case: .notRequested)

Is nicer to read than

values
  .compactMap {
    $0.notRequestedWithShouldRequest
  }

I will not stress enough to repeat that we are talking about another proposal in a pitch that has nothing to do with it. I don't want to debate about properties over associatedValue methods. This could be a very interesting debate, but I believe it should happen in another thread. Properties have another topic on which the solution was debated. Rejected or not, ultimately got abandoned, and never became an official proposal. This topic is not intended to grave dig the other one. You would like to see the other proposal getting alive, noted that. Please give me the chance to debate this one that is feasible, I proved it with code that you can copy paste in a playground and can be greatly improved with the help of the compiler that already knows how to do pattern matching in ifs and guards. There is enough literature for synthesized properties in the dedicated topic. Please allow me to grab some feedback from the community on this one too.

Hey, it's not my intention to block the discussion, I genuinely want to contribute, and the use cases that are tackled by your strategy are the same that one would consider with a property-based approach, so I think my points are relevant to the discussion (as an alternative to consider, for example).

Why wouldn't it? I have had the same kind of problem with RxSwift, and solved with compactMap + computed property (map + filter + map before RxSwift 5), without the need to extend Observable.

I personally think that primitives + composition is a better approach, in the long term, than extending types with custom methods, and in this specific case I can say that those generated properties have helped me a lot in my Swift code for a long time, so I have some grounds to support my impressions on this matter.

ignoring the slightly condescending tone, I've yet to see a single use case where the approach you propose is better, for example you say that:

we can discuss about readability, by I disagree with the fact that your code is more readable, and actually the code you presented has a flaw, because the enum case was actually:

case notRequested(shouldRequest: Bool)

but your code is not representing the shouldRequest label. When you write:

values
  .compactMap {
    $0.associatedValue(case: .notRequested)
  }

I don't see any reference to what the associated value represents. This is even worse:

values
  .map(case: .notRequested)

it might be me, but the first impression I get from this is that it's going to return a [Bool]: true where the case was notRequested and false where it wasn't.

1 Like
values
  .map(case: .notRequested(shouldRequest:))

This works as well. When there is no ambiguity due to overloading, the second part is omissible.
I understand your point of view. Personally, rather than repeat the pattern map -> filter -> map I prefer to write an extension and get over it with one operator wherever I need it.

events
    .capture(event: MyEvent.eventName)

Besides, If I'm explicitly capturing it, I know what the payload represents, there is no need of extra verbosity.

I'm sorry if this was readable as condescending tone. it's not my intention. It's just that I feel like we are going out of scope, and this is getting frustrating.

I updated the initial post to reflect the evolution of the topic

I took the time to go over the entire thread (prior to my response) and answer point-by-point to what @GLanza is proposing.

TLDR: I don't think this is the correct strategy to handle the current shortcomings of enums, the syntax is not convenient and ergonomic enough, and the access to associated types is read-only.

This method is confusing, and not really what you want for extracting the associated value(s) out of an enum, because it's only based on the type. Even if there are merits to the strategy proposed in the pitch, I'd drop this method.

I really can't see those. I use enums a lot, really a lot, because they are the correct tool for representing scenarios with alternative choices, which is something you frequently encounter when modeling. But I never ever wrote an enum with all cases with the same associated type, because in that scenario you would use a struct with 2 properties, one of which is the enum without associated types.

This is actually a useful thing that your proposal is missing, thus a reason to consider your proposal an insufficient solution to the "enums problem". You have to admit that there are cases where "updating" and enum makes sense, that is, mutating it's payload only if it's a certain case, for the exact same reason why you might want to mutate the value of a struct property when the latter is optional. Consider that writing something like:

foo?.bar.baz = 42

means exaclty "mutate the value associated to the property foo if it's in the some case". This means that Swift actually supports and encourages mutating an enum, and that enum is Optional! There's no reason why this shouldn't be applied to other enums as well.

That is simply not true. The reason why you'd use a struct is because you need a "product type", not because you need to make it mutable.

Again, not true, and the use of Optional is exactly a use case for enum mutability. It's not about adding unpredictability: it's more like adding a branching in the code path, an if-then-else.

It wasn't rejected at all. Also, in this post you say that you understand the point of view of @stephencelis, but dismiss it because you're talking about something else, while in the previous posts it seemed that you didn't understand the value of making it easy to mutate enums and use them in keypaths. Are you 100% sure that you got the value of having computed properties for enums? Because mutability and keypath access are major features missing from your pitch which to me is in itself an argument to consider the proposal insufficient to tackle the enums problem.

And again, the fact that enums are awkward to work with is a problem that affects the language as a whole, and you proposal describes exactly a way to address this problem, so it's definitely reasonable to confront it with and stack it against other options.

Agreed, and by access we should consider both just reading the (optional) value and mutating it.

I disagree, it's the same conversation, for trying to address the same problem.

I already addressed this: you simply use the already available methods, and call the computed properties in the lambdas. No one forbids you to write more domain-specific extensions, though, that are still generic, so this is not a problem.

If I understand your proposal correctly, this code wouldn't compile, because the compiler wouldn't understand .notRequested(shouldRequest:): you would be forced to add the type in it, like the following:

values
  .map(case: Location.notRequested(shouldRequest:))

which makes it definitely more awkward than:

values
  .compactMap {
    $0.notRequestedWithShouldRequest
  }

Imagine how bad it would be with enums defined as nested types: you would need to reproduce the entire nesting chain, like Foo.Bar.Baz.caseName(parameter:). To me this is a valid argument against this signature:

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

because Swift doesn't allow us to refer instances of (AssociatedValue) -> Self with the . shorthand syntax.

I do too, in fact, prior to RxSwift 5, I wrote my own compactMap.


I don't want to seem pedantic or rude, and I really appreciate the fact that this pitch is encouraging this discussion (again), but this pitch tries to address the same problem we all have with Swift enums, this is not a different thing that shouldn't be confronted with the computed-property approach, because if this ends up in Swift, it would be because it (partially) addresses the same problem, and this strategy is simply insufficient, considering the dualistic nature of structs and enums, how Swift treats Optional for mutability, and convenience and readability in general.

4 Likes

First of all, thank you for taking the time to go over this proposal and answer so carefully point by point.

Fair, I still believe it's useful, but if the community think it has no use, we should drop it.

Can you bring me an example where you might find useful to use an hypothetical enum update vs a struct? Please don't use Optional as example. Optional is a very special enum surrounded of a lot of compiler magic. In fact, if you define your var as Optional<Int> you get very different behaviors as if you define it as Int?. I would leave Optional out of this conversation, because it would deserve a whole topic for itself.

Personally I don't see any scenario where you want to update a value instead of emitting a new one, or use a struct instead. If you bring me an example maybe I can see the whole thing from a different perspective. Right now I can't and for now I repeat that I believe enums should always be read-only.

Like I said, Optional is very special

The fact that I understand a point of view doesn't mean that I share it. And yes, if I'm talking about accessing associated values in a read-only manner, and you talk about accessing them with read-write rights, we are talking of two different things. For now I want to win the read.

I understand that having computed properties means that no-matter-what you get them, and they will be potentially A LOT, all optionals. I understand that you can't have selective logic like the array example I brought to you: extension Collection where Element: ??? while with a protocol, say CaseAccessible you can.

Totally true. But I don't want them writeable.

This proposal is not the synthesized properties with different flavor. It's not just a different strategy, it has a totally different goal.

I'm proposing 2 methods to be synthesized just if the enum is conforming a protocol, to access an associatedValue in the specific use cases you need to. Swift users still have control over this feature. If they want it they conform the protocol, otherwise they deal with enums the same way they did so far.

In the synthesized properties proposal it was proposed to change the way swift users think about enums, having a computed property to be synthesized for each argument of each associatedValue, accessible via key paths having mutability power. For a simple enum of 2 cases, each having a tuple of 2 you would get (either you want them or not) 6 computed properties with auto generated names that possibly are over verbose to support overoloading.

I'm not changing what enum is, I'm proposing a discrete "extension" that you can decide to get if you need it or not, similarly to CaseIterable.

How would you do that? The Collection extension I defined in the example in my first post works with any CaseAccessible.

extension Collection where Element: CaseAccessible { ... }
extension ObservableType where Element: CaseAccessible { ... }
...

How would you write an extension that works with ANY enum to access their associated values? If by Domain specific you mean that you should write one extension for each enum you have in your app, I hope you understand the amount of work you need to make it happen. In an event based architecture where each enum represents a set of events, I rely a lot on enums.

I disagree. the property name you have there is really confusing, it doesn't explicitly refer to a case, it doesn't explicitly refer to a parameter and being autogenerated you are forcing the developer to write cases in function of what they will have as computed property.
Yeah, I think that

values
  .map(case: Location.notRequested)

is a lot better than

values
  .compactMap {
    $0.notRequestedWithShouldRequest
  }

first of all with map(case:) is explicit which case you want to map, it's very declarative, and it works for any CaseAccessible, ultimately, the name of the case is well known and you don't have to guess which name was assigned to the property by the compiler. User can cmd+click and jump in the enum, option+click will also give them documentation, the full case declaration and the file where it is defined. It works today already, try it out it's awesome. You just can't get these things with automatically synthesized computed properties.

You can stop at caseName if there is no overloading or the context is clear to the compiler. The fact that nested enums have this kind of "problem" is something that currently exist in the language and I'm not trying to solve this "issue". In fact, I don't think I should. And the method signature

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

Is perfectly reflecting the intention of this pitch: give exact context to the reader.

/// This is just a wrapper for Bar that wraps Baz
enum Foo {
    /// This is just a wrapper for Baz
    enum Bar {
         /// This actually contains something
         enum Baz {
              /// Yeah, I nested 3 enums just because I can!!!
              case caseName(parameter: Int)
         }
    }
}

let value = enumCase.associatedValue(matching: Foo.Bar.Baz.caseName)

This is giving me a lot of informations: I want the associated value of a case that is in Baz, nested in Bar, nested in Foo. I can option+click on caseName and I would read "Yeah, I nested 3 enums just because I can!!!", I would read that the complete case is case caseName(parameter: Int). You can option click also on Foo, on Bar and on Baz getting the correct documentation.

If you nested 3 enums knowing that to access the nth nested one you'll have to provide nth-i namespaces, I don't see why I should make properties accessible from the first level enum, losing all the context.

These are effectively the same thing.

3 Likes

At least this function should be called compactMap or filterMap or something to that effect. It's certainly mapping over the cases in values, but it is also filtering/compacting the result to only include the matching cases. The name map doesn't suggest that this is happening.

1 Like

Agreed. I didn't put much thought in the Collection extension as I thought of them more as an example to bring on the table, rather than an actual implementation to include in swift. Do you think it is something we should want in the language?

Can you provide an example? I can't find any.

class Foo { //Error: class has no initializer
    var optional: Optional<Int>
}

while

class Foo { //All good
    var optional: Int?
}

Also var optionalInt: Optional<Int> = .none suffers of all the problems that normal optionals do, due to associated value accessibility. There is no way you can change the value wrapped by Optional in optionalInt, without assigning a new .some or .none case to the optinalInt var. The magic of Optional with ? are not a subject for this thread in my opinion.

Personally, no.

While I value and appreciate how this pitch and discussion has challenged me to think about how to approach enums — and I enjoy the cleverness of using the fact that the compiler synthesise static constructor functions as a way of referencing the cases without their associated values — I think the right approach to the problem is to allow for better and more type safe synthesised properties.

But if the function is to be included, I think it shouldn't be named map, because it is also filtering the input (or compacting the result).

I have previously used to define an inner Case enum for CaseComparable enums, with identically named, but without associated values, and manually added a case: Case computed property to these enums, in order to compare an enum instance to a case without associated values and using it as an expression. I tried to remove all of that, and instead define an infix operator like this:

infix operator ~=: ComparisonPrecedence
extension CaseComparable {
    static func ~=<AssociatedValue> (lhs: Self, rhs: (AssociatedValue) -> Self) -> Bool {
        guard
            let value: AssociatedValue = lhs.decompose()?.value
            else { return false }
        return rhs(value) == lhs
    }
}

And while I appreciate that it allows me to simply declare conformance to most enums without further implementation, I also don't like the ergonomics of having to write

if selection ~= Foo.bar {
  // do something
}

… rather than just if selection ~= .bar at call site.

The same is true for Result, Either, and other both general and specific enums. I have lots of places where I switch over an enum, extract an associated value, mutate (or derive a new) value, and then return (or mutate) the enum. It happes a lot.

I don't know what you mean by this. Associated values of an enum are never updated in place (except as a possible optimizations).

exactly

This is needed due to the fact that the synthesized constructor cannot infer the type of Foo with my solution. I don't know if with some compiler help this can be improved, the point is that I tried to provide a working implementation without the help of any kind of magic. This is code you can copy and paste on a playground today and it will work. Acting on a compiler level probably this can be improved.

Exactly. I think that statement holds for the pitched solution as well. It can be greatly improved with some compiler help, i.e. synthesized and type safe getter and setter accessors, as well as a way of expressing case comparisons (case pattern matching) as an expression.

But I do enjoy this fresh take on the problem, and appreciate that we can use these techniques today, without any kind of magic.

1 Like