Extract Payload for enum cases having associated value

I did this with a playground:

Given an enum

public struct Person {
    var name: String
    var lastName: String
}

public enum Foo {
    case bar(Person)
}

Let's define the class CasePattern. Please ignore current implementation. It was needed to make it compile.

public class CasePattern<Root, Value> {
    public enum PatternError: Error {
        case patternMismatch(String)
    }
    
    internal var get: (Root) throws -> Value
    
    internal init(get: @escaping (Root) throws -> Value) {
        self.get = get
    }
    
    func appending<AppendedValue>(path: KeyPath<Value, AppendedValue>) -> CasePattern<Root, AppendedValue> {
        return CasePattern<Root, AppendedValue> { root throws -> AppendedValue in
            (try self.get(root))[keyPath: path]
        }
    }
}

Now we extend the enum to expose CasePatterns. Again, ignore the implementation. It was needed just to make it compile.

public extension CasePattern where Root == Foo {
    static var bar: CasePattern<Root, Person> {
        CasePattern<Root, Person> {
            guard case let .bar(value) = $0 else {
                throw PatternError.patternMismatch("\($0) does not match pattern .bar")
            }
            return value
        }
    }
}

Now this is possible:

extension Foo {
/// Not possible until https://github.com/apple/swift/pull/22749
//    subscript<Value>(case pattern: CasePattern<Self, Value>)throws -> Value {
//        try pattern.get(self)
//    }

    // Just to make it compile
    subscript<Value>(case pattern: CasePattern<Self, Value>) -> () throws -> Value {
        { try pattern.get(self) }
    }
}

let e = Foo.bar(Person(name: "David", lastName: "Bowie"))


let kp: KeyPath<Person, String> = \.name // regular keyPath for Person to name property
let cp: CasePattern<Foo, Person> = .bar // with the help of the compiler should be \.bar

let cpKp = cp.appending(path: kp) // With the help of the compiler should be \Foo.bar.name
try e[case: cpKp]() // prints "David"

CasePattern and KeyPath can be combined.

And here is the Writable example; Again, ignore please the implementation, it was needed to make it build on a playground. I'm sure that the core team can do better.

public class WritableCasePattern<Root, Value>: CasePattern<Root, Value> {
    internal var set: (inout Root, Value) throws -> Void

    internal init(get: @escaping (Root) throws -> Value, set: @escaping (inout Root, Value) throws -> Void) {
        self.set = set
        super.init(get: get)
    }
    
    func appending<AppendedValue>(path: WritableKeyPath<Value, AppendedValue>) -> WritableCasePattern<Root, AppendedValue> {
        return WritableCasePattern<Root, AppendedValue>(
            get: { (try self.get($0))[keyPath: path] },
            set: {
                var value = (try self.get($0))
                value[keyPath: path] = $1
                try self.set(&$0, value)
        })
    }
}

Let's expose the WritableCasePAttern

public extension CasePattern where Root == Foo { //Replace to previous example extension
    static var bar: WritableCasePattern<Root, Person> {
        WritableCasePattern<Root, Person>(
            get: {
            guard case let .bar(value) = $0 else {
                throw PatternError.patternMismatch("\($0) does not match pattern .bar")
            }
            return value
        }, set: {
            guard case .bar = $0 else {
                throw PatternError.patternMismatch("\($0) does not match pattern .bar")
            }
            $0 = .bar($1)
        })
    }
}

subscript becomes 2

extension Foo {
/// Not possible until https://github.com/apple/swift/pull/22749
//    subscript<Value>(case pattern: CasePattern<Self, Value>) throws -> Value {
//        try pattern.get(self)
//    }

    subscript<Value>(case pattern: CasePattern<Self, Value>) -> () throws -> Value {
        { try pattern.get(self) }
    }
    
/// throws not possible yet, but this should be 
// subscript<Value>(case pattern: WritableCasePattern<Self, Value>) throws -> Value
    subscript<Value>(case pattern: WritableCasePattern<Self, Value>) -> Value {
        get { try! pattern.get(self) } // should not force try
        set { try! pattern.set(&self, newValue) } // should not force try
    }
}

And we have updates with KeyPaths composability

var e = Foo.bar(Person(name: "David", lastName: "Bowie"))

let wcp: WritableCasePattern<Foo, Person> = .bar //With compiler help should be \Foo.bar
let wkp = \Person.name

let wcpKp = wcp.appending(path: wkp) //With compiler help can be \Foo.bar.name
try e[case: wcpKp] = "Frank" // With compiler help can be try e[case: \.bar.name]

print(e) // Prints "Frank Bowie"

We have different write behaviors here. You have a write to (part of) a non-active case be silently ignored, while I have the incoming case take over but be incomplete. Both are bad, but I think your version is how a chain assignment works with Optional intermediate properties (well, actually gets ignored instead of working when there's nil).

The reassignment to .one shouldn't work because \MyEnum.two.b is an EnumKeyPath<MyEnum, Double> while \MyEnum.one is an EnumKeyPath<MyEnum, Void>. The compiler should definitely catch it.

Please check my previous post with CasePattern

Aren't you off by one in your nesting?! I feel so, which makes a big change in your later arguments. I'm thinking of:

// Fundamentals
MyEnum.empty       // Type: (Void) -> MyEnum
MyEnum.void        // Type: ((Void)) -> MyEnum
MyEnum.never       // Type: (Never) -> MyEnum
MyEnum.value       // Type: (Int) -> MyEnum
MyEnum.namedValue  // Type: (Int) -> MyEnum
MyEnum.values      // Type: (Int, String) -> MyEnum
MyEnum.namedValues // Type: (Int, String) -> MyEnum
MyEnum.tuple       // Type: ((Int, String)) -> MyEnum
MyEnum.namedTuple  // Type: ((a: Int, b: String)) -> MyEnum

This affects the 3 Desires, because it makes the key paths between ...values and ...tuple different, since a layer of tuple-ness isn't flattened away. It makes .empty and .void different in Question 1. That would be incompatible how we make single-item tuples go away and generally make them equal to their flattened counterparts.

enum MyEnum {
    case one(a: Int, b: Double)
    case two(c: Double, d: String)
}

Should the same EnumKeyPath<MyEnum, Double> type work for .one.b and .two.c? I would think so; the path should just store an offset into the bitbucket and a note of which tag needs to be active for access.

Yes, it should, except that the CasePattern for \.one should be \.one(a: , b:). Then CasePattern should be combinable with regular KeyPath like I showed with the playground able code example a couple of posts ago.

I don't think so... I copied the types exposed in a playground (Xcode 10.3):

And this compiles:

let a0: MyEnum = MyEnum.empty
let a1: (Void) -> MyEnum = MyEnum.void
let a2: (Never) -> MyEnum = MyEnum.never
let a3: (Int) -> MyEnum = MyEnum.value
let a4: (Int) -> MyEnum = MyEnum.namedValue
let a5: (Int, String) -> MyEnum = MyEnum.values
let a6: (Int, String) -> MyEnum = MyEnum.namedValues
let a7: ((Int, String)) -> MyEnum = MyEnum.tuple
let a8: ((a: Int, b: String)) -> MyEnum = MyEnum.namedTuple

This affects the 3 Desires [...]

Thanks for double-checking :+1:

1 Like

Just because the round trip is a no-op does not make it reasonable. It is semantically dubious. What does it mean to set to nil, especially when that nil does not come from a prior read?

There is a lot of prior art on prisms that we should learn from. The closest analogue in Swift is a getter that returns optional and a throwing setter that accepts a non-optional value. For example, if you have a Result.success value and attempt to write through a Result.failure key path it would throw.

We can ignore semantics and try to make a round peg fit in a square hole (i.e. make a prism look like a lens) but that does not mean it’s a good idea.

5 Likes

I proposed a throwing subscript instead.

subscript<Value>(case pattern: WritableCasePattern<Self, Value>) throws -> Value

try e[case: \.bar] // get a non optional "Value"
try e[case: \.bar] = "bla" // can't pass nil

If you don't care about the success of the operation then

try? e[case: \.bar] // get an optional "Value"
try? e[case: \.bar] = "bla"

What does it mean to pass nil to any function that takes an optional argument? It might mean to use a default value, or to ignore that input, or something else entirely. We can define the semantics if necessary, similar to how setting nil with the Dictionary subscript was given the only reasonable meaning that round trips correctly. I'm not saying that this is the only approach, but it could be one possible reasonable and feasible one, given that it's not clear that subscripts with mismatched get/set types (and therefore enum keypaths that act as prisms) will ever be a Swift feature.

1 Like

I'm "torn" between those two positions... But I think @anandabits makes an important point about throwing setters: they solve a problem optionals can't. The code below has to show, in some way or in other, that the setter can fail:

var result: Result<Player, Error> = .failure(...)
result[keyPath: \.success.score] = 100 // nope

So it really looks like the language can't handle writable enum key paths without... some modification.

1 Like

Ahh, thanks for linking to that. This has been a long thread and I admit I haven't read all of it thoroughly. It could be a reasonable way to make prisms fit into Swift with symmetry for the getter and setter.

Right. Even with the symmetric throwing design Swift doesn't support throwing subscripts and properties yet. That said, this feature has been discussed in the past. I believe @beccadax wrote the latest draft proposal on the topic.

It's worth pointing out that the drafted design provides support for both properties and subscripts and allows for a throwing getter without a throwing setter and vice versa.

Assuming that draft for now, a design might look something like this:

class EnumWritableKeyPath<Root, Value>: KeyPath<Root, Value?> { ... }

// a compiler-provided pseudo-extension
extension Any {
    subscript<Value>(enumKeyPath: EnumKeyPath<Self, Value>) -> Value {
       get throws { ... }
       set throws { ... }
    }
}

The subscript would be available on all types because EnumKeyPath would participate in key path composition. For example, if you have a struct that stores a Result you could form a key path rooted in the struct into the success or failure case of that Result (and further into the payloads of the case if desired).

It would also be desirable to explore supporting a non-failable injection operation for enum key paths which can support it (not all EnumWritableKeyPaths could support this so we would probably need another subclass).

For example, if I have a \Result.success key path and a Result value I should be able to use the key path set the value to success by providing the necessary associated value regardless of the current case of the current value. The difference between setting the associated value and injecting a new, fully formed enum value is subtle but important.

1 Like

After writing the previous post it occurred to me that when we add throwing properties and subscripts to the language we should include support for key paths to these. Once we do that we will have the infrastructure necessary to support enum key paths without needing to introduce a special case EnumWritableKeyPath class. Instead we would be able to build on top of ThrowingKeyPath or whatever we decide to call it. (I do still think we should explore the special case of key paths that can support injection into an abstracted enum case).

So the hierarchy would be:

Keypath%20hierarchy

2 Likes

I don't think that would be quite right because PartialKeyPath requires a non-throwing getter (it just erases the type of Value).

Aside from that, I think we need to see where we land with throwing properties and subscripts first. Brent's design looks good to me but it hasn't been accepted yet (I'm not sure if an implementation has been started or not). Assuming that design is accepted, we need to consider how closely we want to model the distinctions it makes in the key path hierarchy.

Specifically, that design supports the ability to have a non-throwing getter and a throwing setter (and vice versa). Your hierarchy does not. I haven't had time to consider the tradeoffs involved in our options here so I don't have an opinion to offer right now.

1 Like

It was more a question rather than a proposal of hierarchy. :D

Sure, just trying to lay out some of the tradeoffs involved in getting to a hierarchy. :slight_smile:

Also worth mentioning is that there has been some discussion of replacing the key path class hierarchy with protocols eventually. If we do that new avenues for design might open up. For example, we have also talked about eventually allowing protocols to abstract over effects (such as throws). In the fulness of time, those changes could help to model key paths more accurately without a complicated hierarchy that models every nuance directly.

2 Likes

Gathering my ideas with those of @GLanza, @anandabits and @stephencelis, I propose a gist where I explore, and test, a model of read-only key paths able to unify structs and enums.

You can paste it in a playground, and run it with Xcode 10.3+.

All it takes is a departure from a hierarchy of key path classes. Instead, the experiment defines a hierarchy of key path protocols. This hierarchy sports multiple inheritance, and this is why classes are unfit.

It uses the fact that in Swift, (A) -> B is a subtype of (A) throws -> B. In the gist, enums expose throwing getters.

+----------------------------+          +--------------------+
| AnyThrowingKeyPathProtocol | -------> | AnyKeyPathProtocol |
+----------------------------+          +--------------------+
            |                                    |
            |                                    |
            v                                    v
+--------------------------------+      +------------------------+
| PartialThrowingKeyPathProtocol | ---> | PartialKeyPathProtocol |
+--------------------------------+      +------------------------+
            |                                    |
            |                                    |
            v                                    v
+-------------------------+             +-----------------+
| ThrowingKeyPathProtocol | ----------> | KeyPathProtocol |
+-------------------------+             +-----------------+

I believe this experiment could also be run with optionals, where enums expose optional getters, because (A) -> B is also a subtype of (A) -> B?. However, it can be much more difficult to achieve, because I'm not sure this co(ntra)variance applies to associated types of protocols.

Edit: Despite my sample code, I don't like those throwing getters at all. Ideally, enums would return optionals, because this is a case of simple domain error, just like String.toInt() -> Int?. I'm fond of the Error Handling Rationale and Proposal, it is my pillow book.

There's a simpler approach than this, which is to introduce "prisms", the dual to "lenses" which are modeled by WritableKeyPath. To access storage that might not be there, we need something that you can think of as being an Optional<inout T> as opposed to an inout Optional<T>—the storage itself may or may not exist. While you can read out of either as a T? value, you'd only be able to write back to the former as a T, and the write operation may go to a black hole. KeyPaths already support optional chaining, which has this exact problem, and that's why they're currently modeled only as read-only KeyPaths.

What we could do is introduce an OptionalWritableKeyPath as an alternative subclass of KeyPath. Unlike WritableKeyPath, which lets you read or write back the same type like a subscript or property, OptionalWritableKeyPath has asymmetric read and write operations—you can read out a T?, but you can only write back a T. This concept could cover optional chaining as well as key paths that access through enum cases, dynamic casts, or other potentially failable projections.

(If you want to get even more general, prisms and lenses are both instances of "traversals", which include even more general relationships, such as a projection of a field through a collection of items, allowing you to either read all the fields of all of the elements as an array, or write a single value back out to all of them. In that case, the "read" type would be [T] and the "write" type would be T.)

23 Likes