Automatically derive properties for enum cases

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: myEnum.case.foo 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:

isHidden
isEnabled
isFocused
isSelected
isHighlighted
isTracking
isFirstResponder
isTouchInside

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.

Just wanted to note. The big difference is that these properties aren't compiler magic, especially bad magic like generating properties with user specific names. They actually exist and are declared in accordance with the rules of the language. Which additionally means they are valid for absolutely any UIButton instance. This is another way of saying what @xwu and I were trying to point out when speaking about cases without associated values.

I think he was just talking about how enums in general are valuable and how we've historically been at a disadvantage without first-class support (Objective-C days). His example is UIButton's history of expressing these states as a product, resulting in a lot of invalid states that can be represented in the type system and at runtime. Meanwhile, these states can be defined more simply, without invalid states, using sums of products. He's not mentioning these properties as to having anything to do with the proposal here. He's just reinforcing the fact that enums are important and should have the same compiler-level support as structs.

By sums of products you mean enums with associated values?

Yes. All types in the type system can be represented as sums of products. How you choose to compose structs and enums can allow you to make logically invalid states invalid at compile time. If you're new to the concept, I'd recommend reading up on algebraic data types in more depth.

1 Like

You mean, products of sums or sums of products.

They're an equivalence :wink:

Haha, true. That was embarassing :sweat_smile:

Late to the party, but I'd personally like to treat a case as it's own type that has an "is a" relationship to the parent enum type. Because it mirrors the class vs subclass relationship, we already have syntax for different use cases. For example, here's some objects that I've defined for network responses.

enum NetworkResult {
    case success(response: NetworkResponse)
    case failure(reason: FailureReason)
}
enum FailureReason {
    case non200Response(response: NetworkResponse)
    case noAuthToken
    case noInternetConnection
    case unknown(error: Error)
}
struct NetworkResponse {
    var statusCode: Int
    var headerFields: [String: String]
    var body: String
}

These are super useful things to have as enums because the compiler forces devs on my team to handle both the success and failure case when they get a network response, and potentially handle different types of failures differently.

To me, a .success is a type of NetworkResult. A .noInternetConnection is a type of FailureResponse. Reflecting how we use classes and subclasses, I think a case should be a Metatype.

Enum
FailureResponse

EnumCase
FailureResponse.noInternetConnection (this *is* a FailureResponse)

This makes writing a lot of my code much more natural.

if let result as? NetworkResult.success(let response) where response.statusCode == 200 {
   // Unlike synthesis, the compiler can now reflect that we already know 100% what this case is and avoid optionals
   result.response.headerFields.forEach { print(\($0):\($1)) }
}
switch networkResult {
   // Unlike the current situation we can evaluate case as a bool without caring about associated values
   case failure(let reason) where !(failureReason is .unknown):
       showErrorMessage(reason: reason)
}

I think the synthesis is fine, and I've been for it in the past mostly because it seems like changing enums at this level would delay ABI stability and the core team seemed against it, but I think using syntax we already have makes working with enums a lot nicer. As a side note, I agree with Xiaodi that we should get rid of enums with the same case name but different associated values. I don't see any reasonable use case for that.

1 Like