Skip to content

Commit

Permalink
Key Path Expressions as Functions (#977)
Browse files Browse the repository at this point in the history
* Key Path Literal Function Expressions

* Schedule review
  • Loading branch information
stephencelis authored and airspeedswift committed Mar 14, 2019
1 parent b5bbc5a commit 54d85bb
Showing 1 changed file with 150 additions and 0 deletions.
150 changes: 150 additions & 0 deletions proposals/NNNN-key-path-literal-function-expressions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Key Path Expressions as Functions

* Proposal: [SE-0247](0247-key-path-literal-function-expressions.md)
* Authors: [Stephen Celis](https://github.com/stephencelis), [Greg Titus](https://github.com/gregomni)
* Review Manager: [Ben Cohen](https://github.com/airspeedswift)
* Status: **Scheduled (March 18 - 26, 2019)**
* Implementation: [apple/swift#19448](https://github.com/apple/swift/pull/19448)

<!--
*During the review process, add the following fields as needed:*
* Decision Notes: [Rationale](https://forums.swift.org/), [Additional Commentary](https://forums.swift.org/)
* Bugs: [SR-NNNN](https://bugs.swift.org/browse/SR-NNNN), [SR-MMMM](https://bugs.swift.org/browse/SR-MMMM)
* Previous Revision: [1](https://github.com/apple/swift-evolution/blob/...commit-ID.../proposals/NNNN-filename.md)
* Previous Proposal: [SE-XXXX](XXXX-filename.md)
-->

## 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](https://forums.swift.org/t/key-path-expressions-as-functions/19587)

Previous discussions:

- [Allow key path literal syntax in expressions expecting function type](https://forums.swift.org/t/allow-key-path-literal-syntax-in-expressions-expecting-function-type/16453)
- [Key path getter promotion](https://forums.swift.org/t/key-path-getter-promotion/11185)
- [[Pitch] KeyPath based map, flatMap, filter](https://forums.swift.org/t/pitch-keypath-based-map-flatmap-filter/6266)

## Motivation

One-off closures that traverse from a root type to a value are common in Swift. Consider the following `User` struct:

```swift
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:

```swift
users.map { $0.email }
```

Similarly, `filter` can collect an array of admins:

```swift
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](https://forums.swift.org/t/pitch-support-for-map-and-flatmap-with-smart-key-paths/6073) adding `map`, `flatMap`, and `compactMap` overloads that accept key paths as input. Popular libraries [define overloads](https://github.com/ReactiveCocoa/ReactiveSwift/search?utf8=✓&q=KeyPath&type=) 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:

```swift
users.map(\.email)

users.filter(\.isAdmin)
```

## Detailed design

As implemented in [apple/swift#19448](https://github.com/apple/swift/pull/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:

``` swift
users.map(\.email)
```

Is equivalent to:

``` swift
users.map { $0[keyPath: \User.email] }
```

The implementation is limited to key path literal expressions (for now), which means the following is not allowed:

``` swift
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:

``` swift
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:

``` swift
// 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](https://forums.swift.org/t/key-path-expressions-as-functions/19587/4) 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](https://github.com/apple/swift/pull/19448) that it might be appropriate to define an `ExpressibleByKeyPathLiteral` protocol, though discussion in [the proposal thread](https://forums.swift.org/t/key-path-expressions-as-functions/19587/14) questioned the limited utility of such a protocol.

## Alternatives considered

### `^` prefix operator

The `^` prefix operator offers a common third party solution for many users:

```Swift
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](#future-direction) of `Callable` or `ExpressibleByKeyPathLiteral` protocols.

0 comments on commit 54d85bb

Please sign in to comment.