the best explanation i've come up with thus far is that the runtime crash is due to this logic which seemingly attempts to handle the scenario where an optionally-chained key path component resolves to nil. it looks like there is perhaps an expectation that the presence of any optional-chain component implies that the 'leaf value' of the key path must itself be optional, which is not the case in the provided example. there are even some assertions to this effect in the stdlib, though i'm not really sure how to enable them (or if that requires a custom build of the runtime).
i think the expected behavior in this case would be to return nil
rather than invoke the extension on Optional
, since optional chaining is supposed to 'short circuit' evaluation when a nil
is encountered in a chained expression. for example, if we consider a non-key-path formulation, the compound expression would produce an optional Int value:
let baz = Foo().bar?.baz[default: 5] // evaluates to nil since `bar` is nil
type(of: baz) // Optional<Int>
and indeed, if we modify the example slightly to use an extension that requires no arguments, then we can see that the full key path expression is inferred to yield an optional value when an optional-chain component is present:
extension Optional where Wrapped == Int {
var defaultValue: Int { 5 }
}
let foo = Foo()
let kp = \Foo.bar?.baz.defaultValue
type(of: kp) // KeyPath<Foo, Int?>
_ = foo[keyPath: kp] // nil β no crash!
interestingly, using the original example, trying to provide a full key path expression statically rather than building it up dynamically using the appending(path:)
methods does not appear to compile (at least in any configuration i could think of), which is perhaps another indication that this scenario is an unhandled edge case.
let staticKPE = \Foo.bar?.baz[default: 5] // π A Swift key path must begin with a type
so, putting the pieces together, it seems like the crash is likely due to the aforementioned forced cast to a non-optional value failing at runtime when the key path contains an optional chain, but ends in a non-optional terminal value. this suggests that a workaround may be to update the Optional
extension to return an implicitly unwrapped optional, and indeed, if we make that adjustment the code no longer crashes.
extension Optional {
subscript(default default: Wrapped) -> Wrapped! {
self ?? `default`
}
}
let foo = Foo()
let kp1 = \Foo.bar
let kp2 = kp1.appending(path: \.?.baz)
let kp3 = kp2.appending(path: \.[default: 5])
foo[keyPath: kp3] // nil
we don't get the 'nil coalescing' behavior that the extension is perhaps trying to provide, but i think that is expected in this case.