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
@keyPathMemberLookup
type does not contain a member with the given name. This privileges the members of the@keyPathMemberLookup
type (e.g.,Lens<Rectangle>
), hiding those of whatever type is at the root of the key path (e.g.,Rectangle
). -
@keyPathMemberLookup
can only be written directly on the definition of a type, not an extension of that type. - A
@keyPathMemberLookup
type must define a subscript with a single, non-variadic parameter whose argument label iskeyPathMember
and 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