Extract Payload for enum cases having associated value

I didn't read the gist yet, but I will tomorrow. Just wanted to give my 2 cents about the nil, instead of throwing. In cases where the associated value is optional we would loose the information of "not the right case" vs "right case, empty associated value".

Note how the get() function of Result throws. This would be in accordance with that choice.

There's no information lost, you'd just be distinguishing between nil and .some(nil) which is perfectly valid today. The ergonomics would be perfectly reasonable:

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

let x = Foo.bar(nil)
let y = Foo.baz

if let bar = x[keyPath: \.bar] {
    print("It was bar")
    if let value = bar {
        print("...and had an associated value")
    } else {
        print("...and had no associated value")
    }
} else {
    print("It was not bar")
}

I understand that, but recently we flattened ?? for try?. I believe that if possible it would be nice to not have optionals of optionals.

Result.get() throws because Result's intended use case is for suspending and resuming possibly-throwing operations, and get is the "resume" operator. I don't think it's an appropriate analogy.

4 Likes

Thanks for the post, Joe! It'd definitely be nice to design for something that folds optional chaining into this. I'm not sure I understand this part of your post, though. Are you suggesting that OptionalWritableKeyPath sets go into the black hole? Or that current semantics do?

I had a similar thought but hadn't had time to chime in till now. Other languages model prism setter operations by returning an Either/Result where one case represents a successful set operation and the other represents failure, so a throwing subscript makes sense to me as the "Swifty" way to capture this.

I'd like to refresh this conversation around ergonomics and the pushback against "generated properties" by bringing up an alternative that I thought I mentioned earlier on:

Swift could introduce a brand new trailing ? syntax for unwrapping enum associated values. I've chatted with core team members about this before and it'd be a nice, non-breaking, non-ambiguous syntax that would allow for computed properties to live in harmony on enums with cases of the same name:

result.success? // Value?
1 Like

I'm not sure that's unambiguous, since you would still be able to compose further postfix operations to the end of the expression.

Ah of course. I do wonder if there's a path forward in unifying optional chaining, optional properties, and enum prisms, even if some source breakage needs to happen.

But maybe the default (for now) is to parenthesize before the trailing ?. If you explicitly parenthesize afterwards you can disambiguate.

enum Foo {
  case bar(String)
  var bar: String? { nil }
}

Foo.bar("hello").bar?.count // nil
// same as
(Foo.bar("hello").bar)?.count // nil
// vs.
(Foo.bar("hello").bar?)?.count // some(5)

Are you suggesting the write would go into a black hole if the current value has a different case? Just dropping writes in this case seems like a design that would lead to bugs. Why shouldn’t the write throw when the value has the wrong case? As I mentioned upthread, injection could be offered as an additional / alternative operation to the setter that does not need to throw (or fail silently) when the case is the last component of the path.

2 Likes

There are benefits to throwing, sure. That would also allow the successful path to be a symmetric operation in reads and writes, which fits better into the existing storage model. Enum key paths you can write through are sort of jumping the gun on two points, though: we don't have throwing lvalue accesses yet, and we don't have mutation for enums at all either. The problem of "prisms" already comes up in the existing stable of key path components with optional chaining. I think a reasonable path forward that wouldn't require source breakage might be:

  • Introduce enum key path components as read-only components that produce Payload? values
  • In a future Swift that supports all the necessary prerequisites, we can introduce MaybeWritableKeyPath<T, U> as a subclass of KeyPath<T, U?>, and transparently upgrade optional chaining and enum payload components to this subclass. This would allow the old read-only key path interface to work by producing optionals, and we can introduce a new throwing key path accessor that gives writable access to the storage but throws if it doesn't exist.
8 Likes

I like the idea but I'm not sure I understand how it's less source-breakage. Wouldn't an existing enum with a case and a property of the same name still break without syntax to disambiguate?

Adding new declaration names can break source in various annoying ways, but we generally consider that to be acceptable.

4 Likes

Thank you. :slight_smile:

Nested optionals can obviously be used and misused just as nested arrays can. But they just as obviously have utility, a clear and unambiguous syntax, and often map well to well-defined semantics.

Removing nested optionals should be a clear anti-goal, since it would reduce expressivity and make it harder to solve problems.

That said, they could probably need better ergonomics, especially when you’re dealing with combinations of Any? and casting, and you only care about the innermost value.

1 Like

I have no doubt about it, never said the contrary.

Never asked to remove nested optionals from the language. Just, if possible that I would prefer not to see them in this context.

Last thing I would like to see is

enum Foo { case bar(String?) }

let val = e[case: \.bar] // String??

if let optionalAssociatedValue = e[case: \.bar],  // String?
    let associatedValue = optionalAssociatedValue { ... } // String

// vs

let val = try? e[case: \.bar] // String? 
if let associatedValue = try? e[case: \.bar] { ... } // String

The difference between the two is that if for my algorithm "not bar" and "is bar but nil" is the same thing I can still do try? and get an unwrapped associated value from the if let.

Writing try? is an explicit and significative decision that the developer can take to say "I don't care about the error, drop it". With double optional you can't ignore that this operation can fail.

This is just my opinion.

You can also do

if case let associatedValue?? = e[case: \.bar] {
  // ...
}

There’s lots of ways to deal with both optionals and throwing expressions. I don’t have very strong opinions, but at least there is precedent for using optional subscript getters in dictionaries, and there is language support for pattern matching nested enums.

1 Like

I don't have a strong opinion either. but I do have a preference. If it comes to vote, I would go for throwable getter and setter, rather than an asymmetric alternative.

If we are concerned about collisions with instance properties on the enum, how about a syntax something like foo.case blah, or foo.case blah(blah:blah:) to disambiguate payload labels, to access a payload optionally? (And by analogy, \Enum.case blah(blah:blah:) to get the key path for it.)

I like it. But it's not a KeyPath, that would be another object, right? A subclass?

As I mentioned above, it could be a KeyPath today, and we could introduce a subclass for conditionally-accessible writable keypaths in the future. Since key path objects are instantiated by the runtime, the future runtime could transparently instantiate eligible key paths to be the new subclass in that future version of Swift.

I'm sorry if you had to repeat yourself. I believed that the new syntax would change things.