Automatically derive properties for enum cases

I can test an optional for nil in an expression context. I cannot pattern match in an expression context. In some contexts an expression leads to much more clear and concise code.

Swift is not a minimalist language and is not shy about introducing more than one way to do the same thing where a more concise option can make common code more clear an concise. Testing the case of an enum is necessary often enough that I think improved syntax for doing so is warranted.

1 Like

To be fair, I think even the toys tonight show value. You can imagine expanding them to much deeper data structures. Let's say you want to traverse into an array where a deep key path resolves to a very specific case that has no associated values: if we don't generate those properties and key paths, we can't!

Essentially, I'm for a proposal that makes it easy to convert between an enum that has many cases with associated values and Optional.

I have long said that, in my opinion, the ideal design (in a future version of Swift with value subtyping) would be bona fide subtyping relationships between such enums and Optional. With SE-0155 and the distinction between a single tuple associated value and multiple associated values, that future is now likely impossible, but this proposal would recover most of those benefits almost as ergonomically.

Finding the if case syntax unwieldy is quite another issue, in my opinion, and as far as I can tell, the whole business of trying to design for what you call the "edge case" is about trying to solve that issue. I think it can be subset out of this proposal.

But as you show, filter can be used with a predicate that compares specific enum cases just as well as it can be used with a predicate that uses a Boolean expression. One of these requires more lines of code, but again I think that's a different issue from not being able to chain deeply nested associated values.

Sure you can: you use the path to the enum instead of the case, then write if case.... Again, I understand the point that if case... is a statement, but that's fundamentally a different shortcoming than not being able to chain deeply nested associated values.

This is an argument where your "just as well" is at odds with mine. I share @anandabits view that we should be able to test cases in a succinct way rather than come up with the team's preference for if case let, if case let-else, or guard case let. Are you saying that these two cases are "just as well" the same?

trafficLights.filter { $0.isGreen }.count
// or trafficLights.filter { $0.green != nil }.count

// vs.

trafficLights.filter {
  if case .green = $0 {
    return true
  } else {
    return false
  }
}.count

Mmm, Swift is not minimalist, and there's certainly more than one way about doing certain things, but I think we should certainly be shy about introducing more. By this I mean, it should clear a particularly high bar.

1 Like

Sorry, my points throughout this thread should be asterisked with "as an expression." I want to write as much code as possible with expressions and Swift usually lets me. Non-equatable enums have a sad story in this regard right now and are why I'm writing this proposal in the first place.

Clearly, one is more succinct. But is it a clear enough win to justify compiler magic? I think that bar is much higher than the difference between the two cases here, and so as far as evaluating suitability for compiler magic I would indeed consider the two cases to be "just as well" the same.

IMO the appropriate bar is that a common case sees a significant improvement in clarity and concision. When that bar is met with a straightforward solution I don’t think we should be shy about introdocuing the shorthand unless an even better solution is available.

It was said by a core team member, in another thread that I'm not going to find at the moment, that it's sensible for if and other statements to be acceptable as expressions (eventually). With that change (or even a match expression), no compiler magic would be necessary to make everything utterable "as an expression," yet the inability to interrogate deeply nested associated values ergonomically would persist. This is one illustration of how you have two discrete issues, and why I don't think this proposal needs to address the former.

Making if and switch expressions would be great! I don't have a lot of confidence that we'll see it anytime soon, though, and even if we do, ML languages like Haskell and PureScript still have lenses and prisms to access data in a succinct manner because pattern matching and if expressions are still more verbose than what we want here.

The point I'm trying to make is this: the thought experiment of implementing if expressions demonstrates that you are trying to solve two discrete, and therefore separable, issues.

Generating properties only for enum cases with associated values isn't merely a partial solution to both issues but rather a full solution to one of these issues. This is why I disagree that "cases with no values need a story" in a proposal about cases with associated values.

As we discuss an idea I think it's critical to be clear not only about the proposed solution and its consequences for the language, but also what exact problems we are attempting to address and how completely they are addressed.

If the problem statement is instead "I want to write as much code as possible with expressions," then the solution presented is indeed incomplete in all its variations; we'd need to start talking about ways to safely unwrap an optional value without trapping (rather than merely propagate it) without writing if let or other statements.

I'm not following, sorry. Separating these cases means privileging one for the key path world and leaving the other in the dust, regardless of future means of interrogation (expressions, ~=).

Please let me know what's unclear about the exact problems I'm attempting to address and how completely they're addressed:

  1. Current means of traversing and testing enum cases is verbose and inexpressive. (They will continue to be cumbersome even if/when the language allows for if and switch expressions.)
  2. Enums don't have a key path story. Key paths have shown value as a compiler-generated lens mechanism for structs. Building a story for enums and compiler-generated prisms/traversals is important.

I notice you've changed your problem statement!


Problem 1 in your draft text is adequately solved by the proposed solution:

Boilerplate: Developers regularly write verbose boilerplate to handle associated values. This is a serious ergonomics issue that lingers from Swift's inception.

Solving that problem doesn't require synthesizing properties for enum cases without associated values, however.


Problem 2, as written here and in your draft text is also adequately addressed by the proposed solution.

For cases with no associated value, synthesizing an accessor to a nonexistent payload is...well, that's why we're all calling this the "edge case." It's not clear to me that keypaths would be any less complete without such a paradoxical feature. So again, I think the issue is adequately addressed without synthesizing properties for cases without associated values.


The proposed solution does not actually solve the problem where "testing enum cases is verbose and inexpressive" (your reformulated problem 1) but rather presents a workaround by compiler magic. [To be clear, I'm not voicing an opinion as to whether I agree with the problem statement or not.] With the proposed solution, actually testing enum cases doesn't become more expressive; rather, the proposal is to synthesize members that substitute for those enum cases for the purposes of testing, which in turn rely on existing compiler support to be more ergonomic to use.

I think this reformulated problem and the presented solution are rather mismatched; your draft text, on the other hand, goes into a wonderful discussion under the headings of "Associated Values," "Deep Nesting," and "Key Paths" all of which are laser focused on cases with associated values, and (not surprisingly, therefore) that's where you've proved the worth of your solution.

I haven't. It's still:

  1. Boilerplate
  2. Key paths

These are the main two points included in a very early draft of my proposal: https://github.com/stephencelis/swift-evolution/blob/enum-case-property-derivation/proposals/0000-automatically-derive-properties-for-enum-cases.md

This isn't a paradoxical solution. Such properties and key paths to test enum cases are easy to imagine and use. Key paths would be definitionally incomplete if these cases were omitted.

You're saying it doesn't, but you don't explain. Why?

You assert that "testing enum cases doesn't become more expressive". (I contend they do). I also don't see why synthesizing members for such testing isn't expressive.

What if the compiler support required for these cases would be less complicated than special casing to eliminate support for them? Does your mind change?

Deep nesting doesn't exclude non-associated enums (as leaves). Neither do key paths.

Stephen, there is not a single example within the "Motivation" section of your text that interrogates a case without an associated value, not within any of those sections even where they're "not excluded." They plainly did not figure into the motivation that drives the solution proposed, at least not at the time of writing.

I'm not saying that the proposed solution isn't expressive (it is). I'm saying that testing enum cases isn't made any more expressive. No part of the proposed solution involves testing the enum case. You're evaluating a synthesized method that substitutes for the enum case. That's a workaround in my book. A solution to improve testing enum cases would be, for example, a concise pattern matching expression syntax.

I don't understand the question here. I think the proposal adequately addresses the originally stated two problems: (a) developers regularly write verbose boilerplate to handle associated values; and (b) enums don't benefit from key paths. IMO, we should prune everything that doesn't advance those goals. With an eye to those goals, attempting to figure out a way to synthesize properties for cases without associated values seems to be a distraction.

Key paths are definitionally incomplete whether or not those cases are omitted; we don't have key paths to methods, for example. It was not a barrier to shipping the implementation as-is, and neither should the lack of key paths to enum cases without associated values. If and when a strong use case can be made for them, they can always be added.

Happy to call them out in a future draft. Implementation is incomplete (and will likely be for awhile without assistance), so draft is likewise.

Again, I'm not sure I understand. Testing an enum case in my solution can be done with an expression, thus it is more expressive. Other solutions could make it expressive as well, but ignore the key path story. "Workaround" assumes a better solution. Do you have one? I feel that this isn't a workaround as it completes the key path story by utilizing properties throughout.

I'm trying to address what appeared to be a fear of compiler-generated code. You seem to disagree that compiler-generated code for non-associated values are useful. I'm not sure why, though. It seems to be that it doesn't quite meet your criteria for inclusion, but you've admitted that it is shorter, more expressive, and I keep contending that it allows for key path use, which you may not have use cases for, but allow for libraries and developers to write code for their data structures in a universal fashion.

Let's look at key paths as they exist today. If a user defined this property, should we omit the key path?

var unit: Void

How about this?

var maybe: Void?

Data structures are just data. Key paths should allow for universal operability with data. I don't think your stance makes enough of an argument for why some parts of data structures should be omitted.

This is a non-argument. Key paths are definitionally incomplete right now because they don't contend with the enum world at all. They're also definitionally incomplete right now because they don't offer tuple member access. Tuple member access is something compiler engineers have agreed would be nice and even a good starter task. Meanwhile, methods have compiler-generated code in the form of static (currently curried) unbound functions. Others have suggested key path resolution to methods (I admit I don't know what that would look like, but regardless, methods produce reusable, compiler-generated code). I don't think your argument makes a lot of sense when placed in context. Tuple key paths weren't omitted because a strong case hasn't been made for them. Method key paths have an existing story (though I'm totally into hearing why method key paths could be interesting!). But imagine if structs decided to generate some properties but not others. That's exactly what you're suggesting for enums.

You're not testing the enum case: that is to say, the enum case is neither the argument of a function call nor the receiver, neither the LHS of an operator nor the RHS. You are testing a substitute for the enum case; namely, the synthesized property. There is no use of the enum case per se which is affected by your proposal.

Again, as of SE-0155, enums with no associated values are not equivalent to enums with an associated value of Void, and both should eventually be able to co-exist in the same enum type. Swift, for better or worse, has completed the distinction between the lack of a value and the presence of a value Void. The payload of a type with no associated value isn't of type Void (or rather, won't be in a short while), as an analogy to your examples here would suggest. It's of no type at all and cannot be expressed in Swift.

Would you omit the keypath to a property of type Never? I'd say there's a good case for doing so. (Yes, imperfect analogy, as the payload of a case without an associated type cannot be said to be of type Never, either.)

Of course it's a valid argument. No aim of your proposal is "completing key paths"; we have no need to aim closer to completeness for its own sake. As I said, I would have no problem if it were impossible to write keypaths for struct properties of type Never, and I see no issue with enum cases without associated values being treated similarly. (Besides, can you even write a keypath for a statically absent member of a @dynamicMemberLookup type? Would you think it's a huge problem if you can't?)

Swift has always reserved compiler synthesis and other conveniences for only the most obvious situations. Some enums are magically Equatable and others are not. Some custom types can have synthesized Codable conformance and others cannot. Some standard library types have conditional Hashable conformances and others do not. It's entirely consistent with the direction of Swift to omit "edge cases" for synthesis.

This is extremely pedantic. Testing can be distilled to evaluating if something is true or false. Let's not go into the weeds here. A synthesized property that tests .myCase = myValue is still testing the enum case.

I addressed this and you're going back into the weeds. You're also not responding to what you quoted.

Really? What's the case? I see no reason to omit that key path, as I see value in the function (Never) -> A.

You said this in the same message, and I still see no reason why these properties should be omitted. If a user chooses to define a property to return Never because they want to work with its associated key path, why should the compiler prohibit them from doing so?

Sounds like a question to bring up in that proposal. I don't see any problem, though. DynamicKeyPath is easy to picture with runtime semantics. What is your point here?

When you say things like "Swift has always reserved compiler synthesis and other conveniences for only the most obvious situations" you are asserting things as fact. This is your impression based on your experience. Swift and its developers, users, and evolution participants shape the language. It isn't this rigid.

In the world of structs, we can generate key paths for all properties. A healthy dual: in the world of enums, we can generate key paths for all cases.

It most certainly does not improve the expressiveness of testing the enum case, any more than encapsulating any other statements in a function improves the expressiveness of the encapsulated statements. This is plainly a workaround, not a solution, to addressing expressiveness of testing enum cases.

Of course I wouldn't omit a keypath to a Void property, just like I wouldn't omit a keypath to a case with a Void associated type. The "weeds" are the details essential for debating the edge case here. We should be neck-deep in weeds, not trying to skate above it.

What value do you see? How do you invoke this function?

In the case of enums where the functionality doesn't exist, I believe that's the wrong question to ask. Rather, why should the compiler enable them to do so?

My point here is that, no, I don't see a problem if you can't spell a keypath to some properties of a struct. Do you?

It is fact that synthesis is reserved for limited and only the most obvious situations today. It's called evolution for a reason: our designs are meant to be consonant with the existing general direction of the language, not strike out against it.

But that's all from me for the evening. I think we've explored this space plenty for now.