Automatically derive properties for enum cases

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:

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.

You keep talking past me here. I specifically say this lets us write things as an expression instead of a statement. This is my definition of expressive. If you’d define your definition of expressive we can settle these circular arguments.

But you would omit a key path to Never. Why?

In (Never) -> A? This function is called absurd in Haskell and other MLs. It allows you to generalize in the type system callbacks for functions in Never. Say you have a non-failing Result<String, Never>. You can call analysis on it, which requires callbacks to extract either side of the case.

result.analysis(ifSuccess: { $0 }, ifFailure: absurd) // -> String

As for value in properties that return Never? I see the same value that comes from functions that return Never.

Yep. And I’ve tried to communicate why. It makes no sense to prevent users from using key paths for properties they define.

Can you provide some citations from compiler engineers for your assertion? How is it fact?

As discussion seems to have focused on cases without associated values:
I don’t think we should add compiler magic for enums with associated values either.
The feature has big potential for confusion, and I doubt it useful enough to justify that.
For me, the biggest enum-usecases by far are Errors and replacing constans with raw-value enums, and I absolutely don’t need generated properties for those.
I expect most real-world code is similar, so most of the magic would never have any benefit.
Of course, I acknowledge that there are usecases where properties of the proposed form can be really beneficial — but for those occasions, you can write trivial code that is easy to understand now, and hopefully use metaprogramming features in the future.

This pitch is precisely the future you talk about. It happens that @stephencelis has a strong motivation for bringing it closer to the present. I’m sure he’s not the only one who looks forward to this feature!

I now think he needs help addressing corner cases in the pitch:

  • Should the focus be put on compiler-generated properties that help one replace statements with expressions, or on keypaths that help digging in deep structures? I mean, if the main motivation is keypath-based optics, then maybe property generation can be avoided - I’m not sure, and I wish I would understand if both motivations have to be addressed in the same pitch, or if we could split the topic.
  • What properties should be generated for case foo vs. case foo(Void)?
  • What is a good answer to people who rightfully say that compiler-generated isFoo properties can generate out-of-place identifiers (I don’t buy the “rename your case” argument mainly because my enum may already be under semver - or my enum is an Apple SDK one)?
  • What is a good answer to people who rightfully say that compiler-generated properties will duplicate Equatable services?

Let’s play my own game:

Should the focus be put on compiler-generated properties that help one replace statements with expressions, or on keypaths that help digging in deep structures?

My own opinion is that properties alone are a good enough motivation. I, too, write many ad-hoc boilerplate properties, and I may well enjoy a little compiler help.

If properties are not enough to complete keypaths for enums, then we’d discuss that later.

What is a good answer to people who rightfully say that compiler-generated isFoo properties can generate out-of-place identifiers (I don’t buy the “rename your case” argument mainly because my enum may already be under semver - or my enum is an Apple SDK one)?

I hope I was able to make it clear that this is a big problem. “Don’t hold it that way” is not quite the good answer.

Two possible answers that avoid this trap:

  1. Opt-in with a dedicated protocol or @whatever modifier.
  2. A “view” of the enum’s case: instead of myEnum.isFoo.

What is a good answer to people who rightfully say that compiler-generated properties will duplicate Equatable services?

I’d answer that it’s a matter of style :-)

What properties should be generated for case foo vs. case foo(Void)?

I unfortunately lack the knowledge. It looks that the answer to this question depends on the way the compiler implements enums.

To me, it looks more like the present of magically generated conformances and features with narrow usecases.
Stuff like Codable could be easy to write even for a Swift novice if the language had powerful introspection and metaprogramming.
Of course, that is a big fish to fry, but in the meantime, imho it‘s more economical to write a simple codegenerator that produces simple results instead of complicated compiler hacking that would produce confusing results for many enums.

I see what you mean, Tino. I had the very same feeling during the Codable introduction.

Yet this pitch is different: it improves the language consistency by filling holes in keypaths.

And as anyone who has used introspection and metaprogramming tools before, I must admit that Swift is taking unexpected paths. I find it fascinating, and I’m very curious.

Key paths are built on top of properties, so I’m not sure we can separate them (someone please correct me if they have a vision for this). Beyond that, they both have their use cases:

  • Properties allow for succinct, expression-based traversal with values at hand
  • Key paths allow us to build compiler-safe code around those traversals for values that aren’t at hand

I’m game for other visions for naming, Bool, and nits around Optional<Void>, but I haven’t gotten any additional exploration in this thread that takes key paths into account.

If we wanna prevent generation for “flat” enums (the only ones that get Equatable for free), I could see that being fine, though I’d appreciate if someone would justify such an omission and explore the consequences.

Metaprogamming is a runtime thing, so you miss out on all the compiler tools, and things like Codable and key paths would move into the world of runtime errors. Introspection is also usually used to refer to runtime support. Are you suggesting something else?

The use cases of Codable and key paths are far reaching and explorations have shown how the Swift compiler can use these tools to make a lot of the code we write today better in a lot of different ways! And this is all type-safe at compile-time! It’s amazing stuff! We are not talking about “narrow usecases” here.

Afaik, introspection usually happens at runtime, but metaprogramming is common to happen at compiletime

I haven’t seen any representative studies on how many people actually use enums that would benefit from those derived properties, but I don’t think they are that useful for error-handling, which imho is what most enums out there are used for (and especially in this context, the change might even be harmful if you can’t opt-out).

Do you mean like Rust hygienic macros? Do you think things like key paths go away when Swift gets a macro system?

Enums are everywhere! Enums are sum types and you can do a lot with them! The industry is largely used to languages not providing first-class support, and Apple can’t explore them fully so long as they need to maintain Objective-C compatibility with their libraries. I encourage you to look at your data in a different way, because just as multiplication and addition are equally important concepts, product and sum types are as well, and they allow us to express invalid states to the compiler. Just two examples off the top of my head:

enum Attachment {
  case video(Video)
  // Video may be a sum type or nest sums more deeply
  case image(Image)
  // Same for Image

enum Id {
  case slug(String)
  // You may want to filter slugs, or traverse into one and manipulate the string
  case id(Int)
  // You may want to map over an optional id and make a request

I don’t really want this thread to be me explaining why enums, key paths, and Codable are useful. Swift has already embraced these concepts. I’ll happily take to another thread or a PM if you’d like more community examples of usefulness (I’m sure others would chime in with other great examples). Part of this proposal is addressing the fact that enums are completely omitted from the key path world. Also, I’d like to point out that even if you don’t directly think you’re using or benefiting from key paths or enums right now, the libraries that the community uses most certainly are.

No (for both questions)

Like exclamation marks? ;-)
In my experince, enthusiasm for enums fades away slightly after you had to migrate two or three larger experiments to other solutions.
Notably, I‘d consider your first example (attachments) to be better solved with a protocol…

But I‘m not even saying the proposed properties are a bad thing - I just don‘t think they are important enough to justify special treatment by the compiler.

As a community, we misuse structs in as many ways as enums are misused, it’s just that we’re used to it since we’ve had structs for so long. We don’t bat an eye at the fact that UIButton has all of the following properties:


which is 2^8=256 different states, most of which are non-sensical. I don’t know how to make sense of a button that is hidden, touch is inside, it is not tracking, is focused, is disabled and is first responder :D

Enums are just new to the Swift community, and so we are being far more discerning with their uses and abuses, which is great.

But, structs and enums are two sides of the same coin, and anything we do for one we should have a corresponding concept for the other. So, structs have key paths generated by the compiler, and people get a lot of use out of those. It seems that enums should also have something that allows us to get at its cases in the same way.

Terms of Service

Privacy Policy

Cookie Policy