Skip to content

Commit 86c7e6a

Browse files
authored
Merge pull request #1455 from Jumhyn/generalize-keypath-conversions
Add generalized keypath-function conversions proposal
2 parents 367a299 + 3ecfac5 commit 86c7e6a

File tree

1 file changed

+99
-0
lines changed

1 file changed

+99
-0
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Subtyping for keypath literals as functions
2+
3+
* Proposal: [SE-0416](0416-keypath-function-subtyping.md)
4+
* Authors: [Frederick Kellison-Linn](https://github.com/jumhyn)
5+
* Review Manager: [John McCall](https://github.com/rjmccall)
6+
* Status: **Active review (December 13, 2023...January 2, 2024)**
7+
* Implementation: [apple/swift#39612](https://github.com/apple/swift/pull/39612)
8+
* Review: ([pitch](https://forums.swift.org/t/pitch-generalize-keypath-to-function-conversions/52681))
9+
10+
## Introduction
11+
12+
Today, keypath literals can only be narrowly converted to a function which exactly matches the argument and return type. This proposal allows key path literals to partake in the full generality of the conversions we allow between arbitrary function types, so that the following code compiles without error:
13+
14+
```swift
15+
let _: (String) -> Int? = \.count
16+
```
17+
18+
## Motivation
19+
20+
[SE-0249](https://github.com/apple/swift-evolution/blob/main/proposals/0249-key-path-literal-function-expressions.md) introduced a conversion between key path literals and function types, which allowed users to write code like the following:
21+
22+
```swift
23+
let strings = ["Hello", "world", "!"]
24+
let counts = strings.map(\.count) // [5, 5, 1]
25+
```
26+
27+
However, SE-0249 does not quite live up to its promise of allowing the equivalent key path construction "wherever it allows (Root) -> Value functions." Function types permit conversions that are covariant in the result type and contravariant in the parameter types, but key path literals require exact type matches. This can lead to some potentially confusing behavior from the compiler:
28+
29+
```swift
30+
struct S {
31+
var x: Int
32+
}
33+
34+
// All of the following are okay...
35+
let f1: (S) -> Int = \.x
36+
let f2: (S) -> Int? = f1
37+
let f3: (S) -> Int? = { $0.x }
38+
let f4: (S) -> Int? = { kp in { root in root[keyPath: kp] } }(\S.x)
39+
let f5: (S) -> Int? = \.x as (S) -> Int
40+
41+
// But the direct conversion fails!
42+
let f6: (S) -> Int? = \.x // <------------------- Error!
43+
```
44+
45+
## Proposed solution
46+
47+
Allow key path literals to be converted freely in the same manner as functions are converted today. This would allow the definition `f6` above to compile without error, in addition to allowing constructions like:
48+
49+
```swift
50+
class Base {
51+
var derived: Derived { Derived() }
52+
}
53+
class Derived: Base {}
54+
55+
let g1: (Derived) -> Base = \Base.derived
56+
```
57+
58+
## Detailed design
59+
60+
Rather than permitting a key path literal with root type `Root` and value type `Value` to only be converted to a function type `(Root) -> Value`, key path literals will be permitted to be converted to any function type which `(Root) -> Value` may be converted to.
61+
62+
The actual key-path-to-function conversion transformation proceeds exactly as before, generating code with the following semantics (adapting an example from SE-0249):
63+
64+
```swift
65+
// You write this:
66+
let f: (User) -> String? = \User.email
67+
68+
// The compiler generates something like this:
69+
let f: (User) -> String? = { kp in { root in root[keyPath: kp] } }(\User.email)
70+
```
71+
72+
## Source compatibility
73+
74+
This proposal allows conversions in some situations that were previously impossible. This can affect source compatibility because overloaded function calls may gain new viable overload candidates.
75+
76+
In typical scenarios, these new candidates will be strictly worse than previous candidates because the new conversion is strictly less favorable. In situations such as:
77+
78+
```swift
79+
func evil<T, U>(_: (T) -> U) { print("generic") }
80+
func evil(_ x: (String) -> Bool?) { print("concrete") }
81+
82+
evil(\String.isEmpty)
83+
```
84+
85+
Swift will (without this proposal) prefer to call the generic function because the conversion necessary for the concrete function is invalid. With this proposal, Swift will still prefer to call the generic function because the concrete function requires an extra conversion (not only does the keypath need to be converted to a function, but the 'natural' type of the keypath function is `(String) -> Bool`, which requires another conversion to get to `(String) -> Bool?`).
86+
87+
However, this is not always true. A newly-viable overload candidate may be disfavored for the key path conversion but favored for other reasons. This should be uncommon, and so the author expects this proposal will have a very small impact in practice, but this will need to be demonstrated as part of landing the proposal in a Swift release.
88+
89+
## Effect on ABI stability
90+
91+
N/A
92+
93+
## Effect on API resilience
94+
95+
N/A
96+
97+
## Acknowledgements
98+
99+
Thanks to [@ChrisOffner](https://forums.swift.org/u/chrisoffner) for kicking off this discussion on the forums to point out the inconsistency here, and to [@jrose](https://forums.swift.org/u/jrose) for assistance in exploring some strange edge cases in the existing behavior of this feature.

0 commit comments

Comments
 (0)