|
| 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