Key Path Member Lookup
Dynamic member lookup allows a type to opt in to extending member lookup ("dot" syntax) for arbitrary member names, turning them into a string that can then be resolved at runtime. Dynamic member lookup allows interoperability with dynamic languages where the members of a particular instance can only be determined at runtime... but no earlier. Dynamic member lookups therefore tend to work with type-erased wrappers around foreign language objects (e.g., PyVal for an arbitrary Python object), which don't provide much static type information.
On the other hand, key paths provide a dynamic representation of a property that can be used to read or write the referenced property. Key paths maintain static type information about the type of the property being accessed, making them a good candidate for abstractly describing a reference to data that is modeled via Swift types. However, key paths can be cumbersome to create and apply. Consider a type Lens<T> that abstractly refers to some value of type T, through which one can read (and possibly write) the value of that T:
struct Lens<T> {
let getter: () -> T
let setter: (T) -> Void
var value: T {
get {
return getter()
}
set {
setter(newValue)
}
}
}
Given some Lens, we would like to produce a new Lens referring to a property of the value produced by the lens. Key paths allow us to write such a projection function directly:
extension Lens {
func project<U>(_ keyPath: WritableKeyPath<T, U>) -> Lens<U> {
return Lens<U>(
getter: { self.value[keyPath: keyPath] },
setter: { self.value[keyPath: keyPath] = $0 })
}
}
As an example, consider a Lens<Rectangle>:
struct Point {
var x, y: Double
}
struct Rectangle {
var topLeft, bottomRight: Point
}
func projections(lens: Lens<Rectangle>) {
let topLeft = lens.project(\.topLeft) // inferred type is Lens<Point>
let top = lens.project(\.topLeft.y) // inferred type is Lens<Double>
}
Forming the projection is a bit unwieldy: it's a call to project in which we need to use \. to then describe the key path. Why not support the most direct syntax to form a lens referring to some part of the stored value, e.g., lens.topLeft or lens.topLeft.y, respectively?
Proposed solution
Introduce a new attribute @keyPathMemberLookup that can be placed on the definition of a type. For such types, "dot" syntax to access a member will be rewritten as a use of a special subscript whose argument is a key path describing the member. Here, we reimplement Lens in terms of @keyPathMemberLookup:
@keyPathMemberLookup
struct Lens<T> {
let getter: () -> T
let setter: (T) -> Void
var value: T {
get {
return getter()
}
set {
setter(newValue)
}
}
subscript<U>(keyPathMember keyPath: WritableKeyPath<T, U>) -> Lens<U> {
return Lens<U>(
getter: { self.value[keyPath: keyPath] },
setter: { self.value[keyPath: keyPath] = $0 })
}
}
Given a Lens<Rectangle> named lens, the expression lens.topLeft will be evaluated as lens[keyPathMember: \.topLeft], allowing normal member accesses on a Lens to produce a new Lens. Such accesses can be chained: the expression lens.topLeft.y will be evaluated as lens[keyPathMember: \.topLeft][keyPathMember: \.y].
Detailed design
The design of @keyPathMemberLookup follows that of @dynamicMemberLookup fairly closely, because both involve alternate interpretations of basic member access ("dot" syntax). Specifically, @keyPathMemberLookup adopts similar restrictions to @dynamicMemberLookup:
- Key path member lookup only applies when the
@keyPathMemberLookuptype does not contain a member with the given name. This privileges the members of the@keyPathMemberLookuptype (e.g.,Lens<Rectangle>), hiding those of whatever type is at the root of the key path (e.g.,Rectangle). @keyPathMemberLookupcan only be written directly on the definition of a type, not an extension of that type.- A
@keyPathMemberLookuptype must define a subscript with a single, non-variadic parameter whose argument label iskeyPathMemberand that accepts one of the key path types (e.g.,KeyPath,WritableKeyPath).
Source compatibility
This is an additive proposal, which makes cuurently ill-formed syntax well-formed but otherwise does not affect existing code. First, only types that opt in to @keyPathMemberLookup will be affected. Second, even for types that adopt @keyPathMemberLookup, the change is source-compatible because the transformation to use subscript(keyPathMember:) is only applied when there is no member of the given name.
Effect on ABI stability
This feature is implementable entirely in the type checker, as (effectively) a syntactic transformation on member access expressions. It therefore has no impact on the ABI.
Effect on API resilience
Adding @keyPathMemberLookup is a resilient change to a type, as is the addition of the subscript