- Proposal: SE-0249
- Authors: Stephen Celis, Greg Titus
- Review Manager: Ben Cohen
- Status: Implemented (Swift 5.2)
- Implementation: apple/swift#26054
This proposal introduces the ability to use the key path expression \Root.value
wherever functions of (Root) -> Value
are allowed.
Swift-evolution thread: Key Path Expressions as Functions
Previous discussions:
- Allow key path literal syntax in expressions expecting function type
- Key path getter promotion
- [Pitch] KeyPath based map, flatMap, filter
One-off closures that traverse from a root type to a value are common in Swift. Consider the following User
struct:
struct User {
let email: String
let isAdmin: Bool
}
Applying map
allows the following code to gather an array of emails from a source user array:
users.map { $0.email }
Similarly, filter
can collect an array of admins:
users.filter { $0.isAdmin }
These ad hoc closures are short and sweet but Swift already has a shorter and sweeter syntax that can describe this: key paths. The Swift forum has previously proposed adding map
, flatMap
, and compactMap
overloads that accept key paths as input. Popular libraries define overloads of their own. Adding an overload per function, though, is a losing battle.
Swift should allow \Root.value
key path expressions wherever it allows (Root) -> Value
functions:
users.map(\.email)
users.filter(\.isAdmin)
As implemented in apple/swift#19448, occurrences of \Root.value
are implicitly converted to key path applications of { $0[keyPath: \Root.value] }
wherever (Root) -> Value
functions are expected. For example:
users.map(\.email)
Is equivalent to:
users.map { $0[keyPath: \User.email] }
The implementation is limited to key path literal expressions (for now), which means the following is not allowed:
let kp = \User.email // KeyPath<User, String>
users.map(kp)
🛑 Cannot convert value of type 'WritableKeyPath<Person, String>' to expected argument type '(Person) throws -> String'
But the following is:
let f1: (User) -> String = \User.email
users.map(f1)
let f2: (User) -> String = \.email
users.map(f2)
let f3 = \User.email as (User) -> String
users.map(f3)
let f4 = \.email as (User) -> String
users.map(f4)
Any key path expression can be used where a function of the same shape is expected. A few more examples include:
// Multi-segment key paths
users.map(\.email.count)
// `self` key paths
[1, nil, 3, nil, 5].compactMap(\.self)
(Note: Added after acceptance to clarify the proposed behavior.)
When inferring the type of a key path literal expression like \Root.value
, the type checker will prefer KeyPath<Root, Value>
or one of its subtypes, but will also allow (Root) -> Value
. If it chooses (Root) -> Value
, the compiler will generate a closure with semantics equivalent to capturing the key path and applying it to the Root
argument. For example:
// You write this:
let f: (User) -> String = \User.email
// The compiler generates something like this:
let f: (User) -> String = { kp in { root in root[keyPath: kp] } }(\User.email)
The compiler may generate any code that has the same semantics as this example; it might not even use a key path at all.
Any side effects of the key path expression are evaluated when the closure is formed, not when it is called. In particular, if the key path contains subscripts, their arguments are evaluated once, when the closure is formed:
var nextIndex = 0
func makeIndex() -> Int {
defer { nextIndex += 1 }
return nextIndex
}
let getFirst: ([Int]) -> Int = \Array<Int>.[makeIndex()] // Calls makeIndex(), gets 0, forms \Array<Int>.[0]
let getSecond: ([Int]) -> Int = \Array<Int>.[makeIndex()] // Calls makeIndex(), gets 1, forms \Array<Int>.[1]
assert(getFirst([1, 2, 3]) == 1) // No matter how many times
assert(getFirst([1, 2, 3]) == 1) // you call getFirst(),
assert(getFirst([1, 2, 3]) == 1) // it always returns root[0].
assert(getSecond([1, 2, 3]) == 2) // No matter how many times
assert(getSecond([1, 2, 3]) == 2) // you call getSecond(),
assert(getSecond([1, 2, 3]) == 2) // it always returns root[1].
This is a purely additive change and has no impact.
It was suggested in the proposal thread that a future direction in Swift would be to introduce a @callable
mechanism or Callable
protocol as a static equivalent of @dynamicCallable
. Functions could be treated as the existential of types that are @callable
, and KeyPath
could be @callable
to adopt the same functionality as this proposal. Such a change would be backwards-compatible with this proposal and does not need to block its implementation.
It was also suggested in the implementation's discussion that it might be appropriate to define an ExpressibleByKeyPathLiteral
protocol, though discussion in the proposal thread questioned the limited utility of such a protocol.
The ^
prefix operator offers a common third party solution for many users:
prefix operator ^
prefix func ^ <Root, Value>(keyPath: KeyPath<Root, Value>) -> (Root) -> Value {
return { root in root[keyPath: keyPath] }
}
users.map(^\.email)
users.filter(^\.isAdmin)
Although handy, it is less readable and less convenient than using key path syntax alone.
There has been some concern expressed that accepting the literal syntax but not key paths may be confusing, though this behavior is in line with how other literals work, and the most general use case will be with literals, not key paths that are passed around. Accepting key paths directly would also be more limiting and prevent exploring the future directions of Callable
or ExpressibleByKeyPathLiteral
protocols.