- Authors: Stephen Celis, Greg Titus
- Review Manager: TBD
- Status: Awaiting review
- Implementation: apple/swift#19448
Introduction
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
Motivation
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.
Proposed solution
Swift should allow \Root.value
key path expressions wherever it allows (Root) -> Value
functions:
users.map(\.email)
users.filter(\.isAdmin)
Detailed design
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)
Effect on source compatibility, ABI stability, and API resilience
This is a purely additive change and has no impact.
Future direction
@callable
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.
ExpressibleByKeyPathLiteral
protocol
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.
Alternatives considered
^
prefix operator
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.
Accept KeyPath
instead of literal expressions
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.