Automatically derive properties for enum cases

Yep! This is on my radar and shouldn't conflict with the current proposal. I just wanted to approach the problem more granularly.

While static cases that take associated values are just that, functions, enum values are data. I don't really see a problem with the example you give.

You can just as well destructure these tuple values today and you have the same readability issues.

switch selection {
case let range(range):
    range.to // Int
case let discreteIndices(discreteIndices):
    range.in.first // Int?
}

Now, functions handily let you specify different label and variable names, so maybe what you're suggesting is to enhance enums with the same? I'm not sure how I feel about that, but it feels like a separate conversation.

This also adds syntactic grammar to the language. Here are a few questions I've posed earlier against alternative suggestions:

This is pending implementation of SE-0155, which you linked me to earlier, unless this has been reconsidered.


If you want a solution which doesn't add new grammar, fair enough. But it's a bit of a stretch to say there's no problem with discreteIndices.in, even without naming guidelines. It reads like Collection has been extended with an in property.

I wasn't, but that would be one way to solve the issue.

This is an interesting find, thanks! I missed it because the behavior still exists today. This is another thing to address in the proposal.

I think new grammar adds a lot of weight to a proposal in general, must be well thought out and consider how it operates with an already complex language as a whole. I also tried to point out that these new grammars should address things like the current lack of key path support.

Are there enum cases in the standard library that might break these guidelines?

I agree that examples like this pose a small readability issue, though we can still reason about things by looking at the case definition.

We can also take a look at some data, though. I just did a quick search through enum cases in a few large projects. I found that the majority are unlabeled, and the majority of those that are labeled are labeled in a data-forward, noun-like way. There are a few exceptions along the lines of:

case transition(to: ViewController, from: ViewController)

But they still kinda make sense using property access.

let toVc = value.transition?.to

In any case, if the readability suffered enough, they could be renamed.

Thanks for your input! I'll make sure it's addressed in the proposal.

1 Like

I've added another alternatives considered section for your suggestion and @anthonylatsis's here:

Let me know if there's anything you'd like to add! The pattern matching grammar in Swift is huge, so any solution that tries to hook into it should consider the ramifications on existing syntax.

2 Likes

I’m a little late to this party, but here are my two cents. The most common trouble I have is that sometimes it’s not ergonomic to use an if case or switch to pattern match an enumeration. Often I want a simple expression. I wonder if adapting as? would work, like:

XCTAssertEqual((foo as? MyEnum.first)?.x, 7)

I'm not sure the type system could support such a thing easily. We'd be dealing with making individual cases a first-class type. I'm sure a compiler engineer could speak better as to the feasibility of such a thing, though!

1 Like

Cases would need to be not only first-class types, but also subtypes of the enum type. There are some other interesting things that would be possible with a fully baked subtyping model for enums, sub-enums, and case themselves. I explored some of the design space in this gist: ValueSubtypeManifesto.md ¡ GitHub. But like you said, the question of feasibility in Swift is best answered by the folks who work on the type system.

1 Like

Hello Stephen, excuse my late response

In case there was a misunderstanding, I was talking about generating isSomeCase properties.

What I meant here is that, having a type SomeType, the compiler shouldn't generate properties whose names are based on the user specific name of the type, like isSomeTypeNice, doesSomeTypeSmoke, isSomeTypeEqualToType and so on. In our case it is an enum case, but the idea is the same. Why does it have to be considered bad code? Perhaps because it is hardcoded and not usable in the general case. I may be somewhat biased, but I am confident the majority will agree with this. All the examples of automation you listed are wonderful of course and have nothing to do with what I'm trying to point out.

I can't be formal and refer to mathematics for instance, because I understand there are various ways to model and interpret things and they are all right as long as they follow the rules, definitions and conventions of what they're built upon. object1.isEqualTo(object2) and object1 == object2 as an example. Swift is a language where arithmetic, comparison and pattern matching operations don't belong to objects, rather, they are similar to how operations over sets are defined in abstract algebra. Most of them are backed by protocols. Because of this, in my opinion, we should hold on to these semantics and keep them uniform throughout the language not to meet inconsistencies and have trouble generalizing because of different models requiring different approaches.

As for an alternate proposal, how about something similar to this?

enum Foo {
    
    case a(Int, Bool)
    case b
    
    func match<T>(_ arg: (T) -> Foo) -> T? {
        
        if ((T) -> Foo).self == ((Int, Bool) -> Foo).self {
            ...
        }
        ...
    }
}
let m = Foo.b

let o = m.match(Foo.a) // (Int, Bool)?

Still not quite sure I'm following. You mean isYellow and isGreen that return Bool? The alternative listed has to return Optional<Void>, which is an awkward API to expose to end developers. The motivations here are pretty clear, and many folks responding to this thread have agreed that isCaseName: Bool property getters feel right.

I still don't really know what you're trying to contribute to conversation here, and I don't really think your statements are accurate. Pattern matching is rooted in ML languages, where it can be quite a bit more powerful, but it does not dictate how values should be accessed. The getters I'm proposing provide a Swift-y model of optics that behaves similarly to languages that are far closer to abstract algebra and category theory than Swift.

How would this work with overlapping case values?

enum Foo {
  case bar(Int)
  case baz(Int)
}

Ignoring the synthesised boolean getter part for a moment, I am torn on the proposed optional payload accessors as described in the original post. I think this kind of automatic synthesis works best when the enum cases have basenames that describe their payload type. For instance, a JSONValue type might look like this (simplified for demonstration purposes):

enum JSONValue {
    case string(String)
    case integer(Int)
    case dictionary([String : JSONValue])
}

In this case, it is pretty useful to have accessors for jsonValue.string returning String?, jsonValue.integer returning Int?, and jsonValue.dictionary returning [String : JSONValue]?. The property names are naturally descriptive.

In other cases, I don't think it's as clear cut. A hypothetical ViewState might look like this:

enum ViewState {
    case loading
    case failed(date: Date, error: Error)
    case succeeded(purchases: [Purchase])
}

For this type, the automatically derived accessors are awkward. If the Void? handling was included for cases without payloads, the loading case would require a formulation like viewState.loading != nil to check if it is indeed loading. Trying to get the purchases means writing something like let purchases = viewState.succeeded. This reads badly. A human would write an extension on ViewState that declared a getter as var purchases: [Purchase?].

I think this automatic derivation fits neatly into some enums and not others. In the types where it doesn't make as much sense, it will add a lot of noise to the enum namespace that is undesirable. As much as it is laborious to type out the accessors by hand, I like being able to craft the enum manually and provide only the accessors that make sense for the particular type's use case. In the ViewState case, that is probably just a declaration of var isLoading: Bool, var purchases: [Purchase]? and var error: Error?.

Most of the automatic stuff would be unused, or at least poorly named. I recognise though in the JSONValue example, all of the synthesised properties would be valuable and it would be very convenient for developers not to have write the monotonous accessors themselves. I would make the same argument for the isCase synthesis; sometimes it is wanted, and other time it is not.

In the proposal it says 'Where existing, user-defined properties collide, Swift cannot derive properties.'. This does not adequately cover my concerns. E.G implementing var purchases: [Purchase?] manually would still mean that var succeeded: [Purchase]? would be automatically derived, with currently no way to prevent that from being generated.

I'm not discounting the concept at all, it clearly is useful in examples like JSONValue, but raising some light on some of the unfortunate coarse edges. Perhaps there is a way to make this wholly opt-in (or opt-out)?

(I put my demo examples into a gist if that is easier for people to read and parse.)

I understand the potential issues with how things read in certain cases, but think the positives outweigh awkwardness here and there. Compiler-generated code is good, and beginning to flesh out a key path story for enums is invaluable. I think if you design an enum that reads poorly for prism-like traversals, you can rename it so that it reads better, or avoid the traversals if they're not useful and then there's no awkwardness in your code. In your example, you're much more likely to exhaustively switch on this data in order to render all states and never encounter the awkward naming.

One could suggest some kind of @attribute to allow for the derived getters to be given a different name, though I'm not sure it's worth it.

Whether you use key paths or not, the compiler generates them for you and they're valuable. Where should we draw the line for compiler-generated code? Right now, enums miss out on the world of key paths, and that's a huuuge loss.

I think there's more discussion to be made here. I'd be OK exploring a discussion on source incompatibility here.

For most other synthesized properties in Swift, we require conformance to a protocol (see Equatable, Hashable, Codable, and CaseIterable synthesis).

Would the same approach be viable here?

Not sure a protocol makes sense here since there are no requirements that are being fulfilled by the synthesized code.

These protocols are able to declare the members that are synthesized. That isn't the case for the properties discussed in this proposal.

I have given this some thought and I lean towards adopting the view that these properties are part of the core capabilities of an enum case. Case declarations are already providing both a factory property or method and a pattern for matching. Taking this approach would mean these properties are exposed for all enums and programmers don't need to think about whether a specific enum supports them or not.

Not saying we should necessarily do this, but you could envision a syntax for accessing these getters only in a keypath expression. They do not have to intrude on the rest of the program (cluttering autocomplete etcetera).

Like you say, an opt-in @attribute feels inappropriate as does the magic protocol idea. I don't have a great answer to propose myself on this front unfortunately.

That is why I proposed not to generate properties for those cases. You said this prevents writing expressive code against all cases (if case let .a = foo). And so we are moving in circles because isCaseName is a bad option from my perspective. Apart from a non-general approach and using user-specific names to generate properties with compound names that I consider to be bad coding practice, the idea is very dependent on the name itself. A senseless name could be generated. I'll try to find an alternative if you really want to cover all the cases. Similarly to the other proposal,

func matches(_ arg: Enum) -> Bool

There isn't any conflict here.

let n = Foo.bar(5)

let m = n.match(Foo.bar) // or Foo.baz

With overlapping case names, in case they are accepted, we can explicitly specify the return type to disambiguate:

enum Foo {
  case bar(Int)
  case bar(Bool)
}

let n1 = Foo.bar(5)
let n2 = Foo.bar(true)

let m1: Bool = n.match(Foo.bar) // generic parameter inferred as Bool
let m2: Int = n.match(Foo.bar) // analogously

Not sure how to deal with chaining though :sweat_smile:

No of course, it doesn't dictate the nature of the operation. The language should. Though not dictate of course, rather encourage. Don't get the wrong idea though, I really am trying to help and find the most suitable way to do this in Swift.

All right, so your argument here is grounded in opinion?

The magic you're doing here doesn't exist in the language. You're passing a function from (Int) -> Foo and there's no way to take this function itself and know what kind of Foo it will return (without evaluation, and evaluation requires construction of the input, which could be impossible, i.e. with custom, user-defined types). You could also pass in any function (Int) -> Foo to the function you've proposed. The static members aren't distinguishable. This design doesn't currently work.

You also continue to make suggestions that are incompatible with key paths and make no attempt to create a compatibility layer. This makes them non-starters. Please consider the three suggestions I gave you earlier so we can avoid talking in circles.

If you consider properties to be equivalent to functions (Root) -> Value, which is something that the existence of key paths further asserts, and you look at how other languages with lenses and prisms define these united stories, a computed property is the most suitable way to do this in Swift.

No, I'm simply having a hard time explaining. I am sure the core team won't like the isCase idea. But, since you want keypaths, generating computed properties really is the only option without making changes to the language. Would you agree leaving the is out?

I see. I haven't noticed that.

I must agree here. * sigh *

As I and others have pointed out, Optional<Void> is the unfortunate alternative here. I'm happy with whatever the consensus is, though. I just want the compiler-generated code! isCase is the primary suggestion because it's the friendliest, readability-wise and API-wise. We can leave it to the review phase to see whether or not isCase: Bool or case: Void? wins.

Regardless, unless I get some help with implementation, it'll be awhile. I'm still puzzling my way through a lot of compiler code.

2 Likes