Read-only keyPath to an Optional

Found this disparity with a keypath to an optional field:

struct Foo {
    var point: CGPoint? = .zero
}

var foo = Foo()
var a = \Foo.point!.x
var b = \Foo.point?.x
foo.point!.x = 42    // ✅
foo.point?.x = 42    // ✅
foo[keyPath: a] = 42 // ✅
foo[keyPath: b] = 42 // 🛑 Cannot assign through subscript: 'b' is a read-only key path

Is this intentional or an oversight?

This is intentional. WritableKeyPath provides the unconditional ability to write to a path and a key path using optional chaining is conditional by nature. We would need a new key path type like OptionalWritableKeyPath that provides the conditional ability for writing.

1 Like

Thinking about how this worked before the advent of key paths, and continues to work, will be helpful. I.e. there has never been a way to assign an optional to the end of an optional chain, unless that value itself were optional. E.g. how can you make only x nil, while leaving a value for y? It's a nonsensical proposition. :tophat::dress::teapot::rabbit2:

foo.point?.x = 42 as Optional // Cannot assign value of type 'Optional<Int>' to type 'CGFloat'
foo.point?.x = nil // 'nil' cannot be assigned to type 'CGFloat'

b is KeyPath<Foo, CGFloat?>. It cannot also be WritableKeyPath<Foo, CGFloat>. The closure signatures look related, but the type system doesn't have a concept of how to relate them like a meatperson does:

{ foo.point?.x } // () -> CGFloat?
{ foo.point?.x = $0 } // (CGFloat) -> Void?

There is optionality in that "setter", but it's not "a CGFloat or no CGFloat", like the "getter". It's "Void if a value was set, and if not, no Void". WriteableKeyPath requires the input and output types to be completely symmetrical—and that optionality flipping breaks the symmetry.

2 Likes

Interestingly (to me), I realized that you can remove writability without a chained path. Is this useful, or just a necessary artifact of supporting chaining?

\Foo.point // WritableKeyPath<Foo, CGPoint?>
\Foo.point? // KeyPath<Foo, CGPoint?>

More isolated:

\CGPoint?.? // KeyPath<CGPoint?, CGPoint?>
\CGPoint?.self // WritableKeyPath<CGPoint?, CGPoint?>

var point: CGPoint? = .init()
point[keyPath: \.?] // {x 0 y 0}
point[keyPath: \.self] = nil
point[keyPath: \.?] = .init() // Cannot assign through subscript: key path is read-only

It's especially weird because it seemingly has nothing to do with this similar-looking, "write-only", Void?-returning expression:

point? = .init()