Automatically derive properties for enum cases

As a general rule, Swift allows you to refer to declarations with compound names by their base name only when it’s unambiguous. You can refer to a method foo.bar(x:) as only foo.bar, for example. The same rule could apply for cases when used as instance members.

7 Likes

Yep, which is nice. My only worry is how we want these to be properties that return values, so the overlap with syntax that generally returns a function bound to self might be strange. I'll definitely include it in the proposal, if only as an "alternative considered."

Yes but it also allows one to disambiguate by spelling out the full name. Though I suppose we could allow that here too, so maybe there's no conflict:

enum Foo {
  case foo(bar: String)
  case foo(bar: Int, baz: Int)
}

let a = Foo.foo(bar: 42, baz: 21)
let b = a.foo
// error: ambiguous
let c: String? = a.foo
// nil
let d: (Int, Int)? = a.foo
// Optional((bar: 42, baz: 21))

And also perhaps:

enum Foo {
  case foo(bar: String)
  case foo(bar: Int, baz: Int)
}

let a = Foo.foo(bar: 42, baz: 21)
let b = a.foo(bar:baz:)

I've pushed a rough draft here:

Feel free to fork my repo and PR or provide feedback in this thread!

2 Likes

First I was like "cool", but as I scrolled down I came to realized how weird this looks. In a sense that it can be considered oversugaring. let foo = .enumCase(args).enumCase and some other examples where it just doesn't seem natural. If I were to face this issue, I would consider an associatedValue property or method, but apparently it is impossible to implement using macro or generics without casting to Any, the latter being inconvenient.

The if case let isn't too bad, but is designed specifically for the conditional operator.
Because we need a similar functionality for a declaration rather than a switch of if statement, we can try and accommodate the case let pattern for declarations

let case .foo(value) = variable  // : ValueType?

But still IMO it's strange and I am not sure how convenient this approach will be for tuples.

@anthonylatsis Did you read through the proposal and the motivation? Anything that you felt wasn't convincing? It's an early draft and I'll try to edit it a bit to be clearer, but the main issues it's addressing:

  1. case let statements require strange, verbose scoping that breaks expressiveness and prevent the usage of single-line closures with implicit return values and better return type inference. This has been a regular enough complaint.
  2. case let verboseness escalates when you're reaching into a deep structure, but properties would allow you to very succinctly optional-chain into them.
  3. Developers often define these kinds of properties from scratch to work around these issues of verboseness and poor ergonomics. This is a code maintenance burden and it's unlikely to be consistently applied to a code base due to that burden.
  4. Maybe most interestingly: enums currently don't function in the key path world. Deriving properties would fix this.

This is still a statement that binds variables, not an expression, so there's no way to act on the value immediately. You also point out that there's the issue with tuples.

The solution I'm suggesting also doesn't need to add any syntactic surface area to the language. It merely utilizes properties in a much-needed way.

Yes, I've read the whole thread. The idea is good, I was talking namely about the discussed way of implementing it. I really think we need to figure out a way to make it look more natural. What do you think of an associatedValue property? Even if it's pure magic under the hood for the sake of making it work, I believe it is a much better alternative.

Yeah, that as well.

I’m not sure what that would look like. Could you provide an example of what that would look like for each of the three examples in the proposal? Could you also think through how optional chaining and key paths would look with such a solution?

I was a bit reckless over there. Such a property would anyway return Any.

In terms of properties, the variant developed in this thread is apparently the best.
I must say it still looks strange, since I see accessing associated values as an unfolding rather than a direct access. That's how pattern matching works. The nature of enums shows this by 'alienating' such semantics .foo(args).foo.

That said, something like the above mentioned let case .foo(value) = variable and an equivalent expression (case .foo(value) = variable) looks more natural to me, though it is also strange and not as short.

Thanks for clarifying. Is this what you meant by feels less “natural”? One way to think of this kind of access is in terms of how key paths on struct properties operate as lenses that can focus on an element and update it. Enums have their own kind of optic, prisms, which can focus on an element and, if it exists, update it. The natural way to test for the existence of a specific case is to optionalize it. I dunno if this explanation makes sense or has you reconsidering, but I do think it’s valuable to describe what feels unnatural, why, and what would feel natural.

I’m definitely open to other solutions, but I still don’t know how this new syntax plays out in the various scenarios I try to address (chaining into a structure, key paths).

I would say it's more like whether it is that case or not..

Key paths to what? Associated values?
Chaining can be done as usual (expression)?. Maybe this way it makes more sense: case .foo(_) = variable
Is what you meant by chaining into a struct something different?

P.S. I will ponder on this further.For now, I would like to propose a small correction: Do not derive properties for cases without associated values at all. I believe letting the compiler construct names from existing ones in such a manner should be discouraged. Apart from that, I think we should leave the ‘isCase’ to the standard methods or otherwise propose a simpler way similar to the standard, but in another pitch of course.

Why shouldn't this be modeled with Optional?

I've updated the proposal to talk a bit more about key paths. For example, the Result type mentioned in the proposal would allow for the following key paths:

\Result<User, String>.value?.name
// KeyPath<Result<User, String>, String?>

\Result<User, String>.other?.count
// KeyPath<Result<User, String>, Int?>

I don't want this post to be about what key paths are good for. The language has already talked about that and embraced them.

You're giving partial examples, but it sounds like an example from my proposal would look more fully like this:

case .result(_)?.anotherCase(_)?.name

// vs.

result.value?.anotherCase?.name

I think new syntax should probably be justified against other options, so what makes the former better than property synthesis? So far you've said it "feels less natural," but it'd be good to have a more practical analysis of the pros and cons of each approach.

Beyond this example rooted in an enum, what does optional-chaining from structs into such enums look like? Does case still prefix it all? What's the overall grammar look like?

If the community bands behind a brand new syntax, I'm totally fine with it (I just want these problems to be solved). I think we can avoid new syntax and look to something that already exists in our language, though: optional properties.

Omitting these cases means no way of working with such cases by default, so folks are back to defining their own properties. Not ideal.

Given:

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

Is a bar property omitted while baz and fizzbuzz properties are generated? How do folks write expressive code against all cases?

Thanks for pushing forward enum ergonomics!

There's a paragraph where you describe using layers of case let syntax:

if
    case let .value(value) = result,
    case let .anotherCase(anotherCase) = value {

        // use `anotherCase.name`  
}

// vs.

result.value?.anotherCase?.name

I don't find this section to be a compelling argument—this example can be pattern matched more succinctly:

if case let .value(.anotherCase(anotherCase)) = result {
    // use `anotherCase.name`
}

@mpangburn Thanks! That's great feedback. What do you think of the following response?


These properties also allow us to traverse deeply into nested structures using optional chaining. E.g.,

result.value?.anotherCase?.name

Without these properties, this becomes much more verbose: we have to wade through layers of pattern matching. The most efficient case of nested enums uses nested pattern matching, but still suffers from an additional statement, variable binding, and scope:

if case let .value(.anotherCase(anotherCase)) = result {
    // use `anotherCase.name`
}

This kind of deep pattern matching isn't commonly known. It's far more common to encounter Swift code that pattern matches over multiple clauses.

if
    case let .value(value) = result,
    case let .anotherCase(anotherCase) = value {

        // use `anotherCase.name`  
}

This is even more difficult to read.

When types are nested between enums, deep pattern matching isn't even possible: pattern matching must be broken up over multiple clauses.

if
    case let .value(myStruct) = result,
    case let .anotherCase(anotherCase) = myStruct.someProperty {

        // use `anotherCase.whatever`
}
// vs.
result.value?.someProperty.anotherCase?.whatever

// or

if
    case let .value(myStruct) = result,
    let firstChild = myStruct.children.first,
    case let .anotherCase(anotherCase) = firstChild {

        // use `anotherCase.whateverStill`
}
// vs.
result.value?.children.first?.anotherCase?.whateverStill

Messier still! Optional-chaining reads nicely: left-to-right. It's much more difficult to follow case binding over multiple clauses, and there are more variables to track and reason about.

2 Likes

In the general case, formally it isn't correct for a comparison (matching) operation for two equivalent by nature objects to be a property. It should be an external operation involving the operands. This is my reasoning and the reason it doesn't look 'natural'.

Yes, of course. So you want to leave space for a future adoption of key paths by enums. We should consider however that the properties of your model as well as associated values in principle are read-only.

Actually it's a bit messier :)

if case let .value(myStruct) = result,
   case let .anotherCase(anotherCase) = myStruct.someProperty {
}
// (case .x = y) -> associatedValue
(case .anotherCase = (case .value = result)?.someOtherProperty)?.name
//vs
result.value?.someOtherProperty.anotherCase?.name

I can't agree that property generation based on existing names should be tolerated. If the generated data structure's name equals that of an existing one, it could be fine as in this case. Otherwise, it is unnecessary hardcoding and misused automation that should be done in code on the user's choice.

Can you give an example of your vision of the code against all cases?

This is deliberately done for semantic purposes and to follow the standard syntax. Your alternative examples are short and tidy, but they lack semantic background. It is very unexpected and not intuitive to read it as it is meant to be.

We pay verboseness for semantics and syntactic formality, although of course there is place for improvements without deviating from these.

In conclusion, I remain sceptical towards the proposed implementation but admit not having a worthy alternative.

Something else worth discussing is if those generated properties could benefit from setters. For example, here is what I generate with Sourcery in my current projects:

enum Section {
    case header
    case info(InfoRow)
    case footer(String)
}

enum InfoRow {
    case name(String)
    case age(Int)
}

// Generated

extension Section {
    var info: InfoRow? {
        get {
            if case .info(let data) = self {
                return data
            } else {
                return nil
            }
        }
        set {
            guard let data = newValue else { fatalError("Can't se without associated value") }
            self = .info(data)
        }
    }

    var footer: String? {
        get {
            if case . footer(let data) = self {
                return data
            } else {
                return nil
            }
        }
        set {
            guard let data = newValue else { fatalError("Can't se without associated value") }
            self = . footer(data)
        }
    }
}

extension Section {
    var name: String? {
        get {
            if case . name(let data) = self {
                return data
            } else {
                return nil
            }
        }
        set {
            guard let data = newValue else { fatalError("Can't se without associated value") }
            self = . name(data)
        }
    }

    var age: Int? {
        get {
            if case . age(let data) = self {
                return data
            } else {
                return nil
            }
        }
        set {
            guard let data = newValue else { fatalError("Can't se without associated value") }
            self = . age(data)
        }
    }
}

This allows transforming:

if case .info(.name(let name)) = section {
    section = .info(.name("David"))
}

into:

section.info?.name = "David"

It also works with structs in the "path" and grows nicely.

3 Likes

I'm beginning to agree that generating properties might not be the best approach, since cases are intended to read like function names.

To give a quick example:

enum Selection {
    case range(from: Int, to: Int)
    case discreteIndices(in: [Int], inverted: Bool)
}

let selection = Selection.range(from: 1, to: 2)
selection.range?.to // = .some(2)
selection.discreteIndices?.in.first

This clearly has some readability issues.


To provide another example following @anthonylatsis's direction, using a matches operator based on @Erica_Sadun's previously suggested pattern matching operator:

selection matches .range(from: _, to: let upperBound) // = .some(2)
(selection matches .discreteIndices(let indices, _))?.first

selection matches let .range(a, b) // produces an (a: Int, b: Int)?

It's a bit more verbose, but it still supports optional chaining while keeps the cases' argument labels in context. This could potentially support setters, although this would be in conflict with precedent set by as?, and is complicated slightly by allowing tuple return values.

I'm really sorry, but I still don't understand. You say it isn't correct, but you don't explain why. This all seems like personal preference.

I am considering this! The almost fully-baked lenses that key paths provide for read-write struct properties are great! The language ought to support prisms for enums just as powerfully in the future. This proposal sets a foundation for that :blush: This proposal also solves other ergonomic problems, though, and there's value to introducing it in a stepwise fashion. There's nothing preventing these property setters from being used like prisms in the future, and I'd be happy to note this in the proposal just as some of the other manifestos (generics, concurrency) have set the stage for features in a stepwise fashion.

You say you don't agree, but you still don't go into why beyond "feels unnatural" and "it is unnecessary hardcoding and misused automation." If the latter is your core issue with the current solution, how do you weigh it against adding syntactic weight to the language to fix these issues? How do you feel about the automation that key paths currently provide? Why automate structs but not enums? How about the automation of generating == for enums with no associated values? Can you specify what the specific criteria is for compiler-generated code? When should something be generated? When and why doesn't it meet the bar?

My existing examples cover all three kinds of cases. I'm happy to provide another example, but I'm curious what's missing from my proposal that requires it. Can you show me what you think my proposal would generate for this type?

I'm sorry, but again you seem to be appealing to "formality" and "purposes" and reasons that don't appear to exist. Can you provide sources and links that explain how this is "deliberately done"? There is no formal reason against modeling prisms in this fashion. My proposal suggests a solution that is similar to how languages like Haskell and PureScript model prism getters. These languages are formal indeed :wink:

A lot of your arguments seem a bit hand-wavey. I really would appreciate if you grounded them a bit more. It's difficult to get to the substance and understand what you're opposing other than "this feels wrong to me."

I'll remain open to other ideas and would really appreciate some suggestions, but they should address, as my proposal does:

  • How does this fix the statement vs. expression problem?
  • How does this fix the current incompatibility with key paths?
  • Is this adding syntactic grammar to the language and is that justified?

If we can come up with other solution(s) that address these problems, then we can weight them against the current proposal and maybe modify it till we get to something even better!

I hope this conversation can be more constructive than destructive, and I encourage you to provide an alternate proposal! I'd love for these problems to be solved, so let's be productive about it!