Allow key path literal syntax in expressions expecting function type

That is, extend the compiler such that \.foo can be used in places that expect functions of type (Root) -> Value , and is simply shorthand for { $0[keyPath: \.foo] }.

Adding this sort of functionality to KeyPath has come up over and over in discussions on sorting ergonomics, on allowing types to be 'dynamically callable' (as an example to make KeyPath 'callable'), and in other sorts of contexts. And it seems like most people are fairly comfortable discussing syntax like: people.sortBy(\.age) and people.map(\.address.street).

I'd like to propose doing the least disruptive thing that would make this sort of syntax possible, which is to treat the key path literal syntax (with \) as if it can be a function type, in addition to being able to produce a nominal type (e.g. KeyPath, WritableKeyPath, etc.)

This would not:

  • Be a way to define any other type as callable.
  • Be a way to create any other type from a key path literal (e.g. This is not an ExpressibleByKeyPathLiteral).
  • Be a coercion between KeyPath and function type (e.g. you can't convert between with as or anything like that.)

But on the plus side, this would extend a syntax that already exists in the language in a very straightforward way.

Here's an implementation if anyone would like to try it out: WIP [ConstraintSystem] Ability to treat key path literal syntax as function type (Root) -> Value. by gregomni · Pull Request #19448 · apple/swift · GitHub

Feedback greatly appreciated, thanks!

27 Likes

Hm, I'm kind of torn. I'd really like to use keypaths in places where function types are expected. However I'd much rather see a solution that enables more than just this one use-case.

On the other hand, this could probably work as a stopgap solution until we have a better one, be that static callable or ExpressibleByKeyPathLiteral. But it might take away what's probably one of the more compelling reasons for building the general-purpose solution, potentially slowing that process.

:thinking:

2 Likes

Indeed literals and type inference often shine together.

The pattern below is classic Swiift, whether we like it or not:

func f(_ x: /* some type that adopts ExpressibleByIntegerLiteral */)
 
f(1) // OK
let x = 1
f(x) // Won't compile

If I understand well, this idea by @gregtitus just builds on this state of fact:

players.map(\.name) // OK
let x = \Player.name
players.map(x)      // Won't compile

This is really interesting :+1:

Edit: It is more than interesting. This is a great idea. Aware of its limits, but also aware of the limits of the surrounding context. It soothes a real itch, without creating any new inconsistency.

Edit 2: No new inconsistency, but a compiler privilege: we still don't have any ExpressibleByKeyPathLiteral protocol that user code could leverage. I don't think this harms the pitch. But the "Future Directions" paragraph of an eventual proposal may want to explore this direction.

2 Likes

I agree with @ahti. I'd prefer to see a more general solution, but I don't think this actually blocks that from happening later (we can just make KeyPaths use the more general solution when we do it)... so a cautious +1 from me.

+1 of me in the current form (limited to literals).

I am curious whether the following code will work too:

let f1 = \Player.name as (Player) -> String
let f2: (Player) -> String = \Player.name
3 Likes

It should; they’re the same thing to the type checker.

1 Like

Yes. Both of these forms work. (I actually had to add a bit to the PR to fix both of these, as I hadn't got a couple technical bits quite right, but it's there now.)

I like the idea proposed here and curious as to why it died off

1 Like

Previous discussion (and pitch proposal) here: Key path getter promotion

2 Likes

I love the functionality, and it seems like a good addition to make KeyPaths more useful.

I also think it's something that could/should be possible with non-KP syntax if we added the ability to generically reference the properties of Types the same way we currently can reference Methods.

e.g.

class Test {
    var thing = 5
    
    func getThing() -> Int {
        return thing
    }
}

let test = Test()

// This works
let genericGet: (Test) -> () -> Int = Test.getThing // ✅

// This works
let specificGet: () -> (Int) = Test.getThing(test) // ✅

// Why can't this work?
let genericProperty: (Test) -> Int = Test.thing // 💥 ERROR 

// Or maybe something like this?
let genericProperty: (Test) -> () -> Int = Test.thing.get // 💥 ERROR 

Using the OP's example, you could then simply do people.sortBy(People.age)
This is probably unrelated to the proposal directly, but I think it solves many of the same problems, with syntax that doesn't require people to understand KeyPaths.

Key paths are how you generically reference properties the same way you reference methods.

Based on the discussion in that thread and on @gregtitus's PR, it looks like there's some amount of consensus around the benefit of using being able to express function types with key-path expressions (#1 below), plus two other areas where proposals could be made.

  1. This pitch: Allow key path expressions to express function types. This whole discussion is basically so that we can all do the obvious thing and use key path expressions inside map calls:

    let names = ["Nate", "Greg", "Stephen"]
    let lengths = names.map(\.count)
    // [4, 4, 7]
    

    This doesn't allow KeyPath instances to convert:

    let kp = \String.count
    let _ = names.map(kp)  // error
    

    which begs the first "extension" proposal…

  2. Extension 1: Provide a way to get a function type out of a key path instance. This is really a convenience, as it's trivial to add this to our own libraries today. Adding the prefix ^ operator is a popular approach. The main benefit here would be to provide a consistent and streamlined way to perform this common operation.

  3. Extension 2: Create an ExpressibleByKeyPathLiteral protocol and augment the existing *LiteralType type aliases. There probably aren't other types besides functions and key paths that would be expressed this way, so the primary purpose of this would be to allow toggling the KeyPathLiteralType alias between those two. Since the existing aliases (IntegerLiteralType &co) predate generic type aliases, they couldn't include ArrayLiteralType or DictionaryLiteralType, so those should be included in such a proposal.

Does that sound like an accurate summary?

1 Like

It's a (newer) option to reference things in a similar way. I'm saying the API for referencing Methods and Properties could/should be the same.

My natural inclination was to try Test.thing and was confused when it didn't work like it does for methods. KeyPaths could/should be the internal mechanism used, but I'm thinking that could be an implementation detail and reserved for more advanced/complex use cases.

The problem with reusing Test.thing is that it doesn't leave any syntax for static members. (This was all considered carefully in the initial proposal for key paths, SE-0161.)

It's also worth mentioning that the plan at this point is to revisit SE-0042 and introduce syntax similar to key paths for unbound method references (rather than incur the source breakage that would be necessary to implement SE-0042 as it was accepted). So consistent syntax is on the horizon, we're just not there yet.