SE-0416: Subtyping for keypath literals as functions

Hello, Swift community.

The review of SE-0416: Subtyping for keypath literals as functions begins now and runs until January 2nd, 2024.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager via the forum messaging feature or email. When contacting the review manager directly, please put "SE-0416" in the subject line.

Try it out

Toolchains with this feature implemented are now available:

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

What is your evaluation of the proposal?

  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at https://github.com/apple/swift-evolution/blob/main/process.md.

Thank you,

John McCall
Review Manager

25 Likes

Makes a lot of sense and in a way actually simplifies the language (by removing a seemingly arbitrary discrepancy).

I try to use keypath syntax in preference to closure syntax wherever possible - it's conciser and at least in my mind more efficient for the compiler if not also at runtime - but sometimes it's a surprisingly frustrating and derailing process because (I think) of the issues that this proposal addresses. The compiler errors when a keypath currently isn't accepted can be highly obtuse to say the least.

Is there a performance impact? Presumably this will greatly expand the candidate functions for a given keypath - especially for some obvious examples like \.count or \.first - and that will impose more work on the type checker (for inference in particular, I assume)?

2 Likes

This is just a straightforward improvement.

3 Likes

It’s true that this may increase the number of candidates, though the compiler shouldn’t need to do any more work than what it would already have to do with the closure { $0.count } in the same position.

3 Likes

I'm really happy that with this map(\.self) and compactMap(\.self) are working. Thanks!

let numbers = [1, 2, 3, nil].compactMap(\.self) // Array<Int> ✨
10 Likes

Is there an error in the code sample immediately above the Detailed Design section? This conversion of KeyPath to a function type is shown:
let g1: (Derived) -> Base = \Base.derived
But should it be this instead?
let g1: (Base) -> Derived = \Base.derived

Could be in misunderstanding how this works, but I can’t see how a KeyPath with root Base and value Derived is converted to a function from Derived to Base.

The \Base.derived keypath function is 'naturally' of type (Base) -> Derived, which accepts any Base value and produces a Derived value. Anything which produces a Derived value necessarily produces a Base value (since Derived: Base) and also a function which accepts Base must necessarily accept any Derived for the same reason. Thus a (Base) -> Derived function may act as a (Derived) -> Base function. The general case is that a function of type (Q) -> R may be a subtype of a function (T) -> U if T: Q and R: U (note the swapped order!). Jargon-wise, function types are 'contravariant' in their argument types and 'covariant' in their return types.

(The \Base.derived as (Base) -> Derived conversion is what's supported today, with an exact match of the types. The (Derived) -> Base conversion is what this proposal enables.)

3 Likes

Ahh, thank you for that explanation. Totally make sense now. And this also explains why you included this as an example.

Will this enable dynamic downcasting from a (T) -> U to KeyPath<T, U>? It may be useful for optimizing a closure call down to accessing a memory offset.

No, this proposal doesn't introduce any sort of general conversion or equivalence between function types and keypath types. It is merely an extension of the rules around what type may be assigned to a key path literal expression.

I'm honestly not sure if my change has fixed this issue or some other near-top-of-tree fix. I know @xedin has put in a lot of effort on cleaning up the existing keypath literal function inference recently :)

(Those conversions should have worked under the old rules as well)

2 Likes

The key path inference has been reworked recently which fixed a lot of cases were key path type was set too early or incorrectly. I've been going through the Github issues, testing and resolving similar problems...

4 Likes

Yeah, now that I'm remembering I think your fix is more likely to be responsible here. The issue when I looked into this previously was that a \.self keypath is eagerly resolvable (since it requires no member lookup) and so we would skip the function conversion path entirely. Delaying that should have had the proper behavior fall out naturally!

Straightforward and a welcome cleanup of a wart.

This proposal interacts strongly with SE-0418, but I don't see details of that interaction in either this proposal or that one.

For completeness, I'll just note that Pavel Yaskevich answered Keith's question in the thread for SE-0418.

3 Likes

SE-0416 has been accepted. Thank you all for participating in Swift Evolution.

John McCall
Review Manager

2 Likes