Key path getter promotion

I'd like to propose a Swift feature that promotes key paths to getter functions wherever appropriate.

It's common to write one-off closures that merely traverse from a root type into a value. For example, take the following User struct:

struct User {
    let email: String
    let isAdmin: Bool
}

Given an array of users, we can use map to gather an array of their emails.

users.map { $0.email }

We can also filter our array of users on isAdmin to gather an array of admins.

users.filter { $0.isAdmin }

These ad hoc closures are short and sweet, but Swift already generates this code in the form of key paths.

The forum has previously proposed adding map and flatMap overloads that accept key paths as input. Popular libraries have added key path overloads, as well.

Adding an overload per function is a losing battle, though. It would be preferable to have first-class support from the language.

Swift should allow key paths of the form KeyPath<Root, Value> wherever functions of the form (Root) -> Value are allowed.

Our earlier examples end up becoming a little bit shorter.

users.map(\.email)

users.filter(\.isAdmin)

This feature would fall in the footsteps of Ruby, which allows Symbol#to_proc to do a similar traversal.

users.map(&:email)

users.filter(&:isAdmin)

If we do not want compiler magic doing the automatic promotion, we could introduce a prefix operator, ^ for example, to the Standard Library. We can define it today:

prefix operator ^

prefix func ^ <Root, Value>(keyPath: KeyPath<Root, Value>) -> (Root) -> Value {
  return { root in root[keyPath: keyPath] }
}

Ideally the compiler handles this for us, though!

23 Likes

There was a good amount of discussion about this feature in this thread: [Pitch] KeyPath based map, flatMap, filter - #14 by Brent_Royal-Gordon

I directly linked to @beccadax's post, which I thought was the most convincing post in that thread. It seems to me there's another option that was called out explicitly in that thread: create specific overloads for map, filter, etc, that take keypaths. I think that option is net worse than both of the options you laid out here (subtyping and custom operator).

Making KeyPath<T, U> a subtype of T -> U makes the most sense to me and would clean up a lot of my code.

This is very appealing. Having worked with this feature in Ruby, I can confirm that although passing keypaths to map and friends is a minor ergonomic improvement, it pays dividends that justify the weight.

The operator alternative is appealing to me; I’ve liked it ever since @SusanCheng described it here.

I would prefer the generality of either of these approaches to method-specific overloads.

Considering whether to allow the language to do the automatic keypath → closure promotion, I wonder:

  • What kinds of type errors would this obscure? Are there places where developer intent would not match the keypath → closure promotion, and the lack of a compiler error would obscure a mistake? (I am looking here for specific, realistic scenarios.)
  • Are there any places where this creates type inference ambiguity, or especially confusing error messages? Or is it essentially a no-burden addition to the type system?

Huge +1 from me. This will greatly enhance the utility of key paths. I favor the subtype relationship as it is likely to come in quite handy in generic code, especially when generalized supertype constraints are implemented.

Part of our motivation for the \. syntax was that it could also be used eventually for forming closures, in the spirit of SE-42, which was accepted and never implemented. Instead of introducing a subtype relationship, we could (and should) extend the key path literal syntax to be valid in function type context, which I think covers the most common use case for this conversion of passing a literal property reference as an argument to map and friends.

In the fullness of time, when we get generalized existentials I'd like to see key paths be reskinned to be more "protocol-oriented" as well. You could think of closures and read-only keypaths as both being instances of an abstract Applicable protocol, and mutable/observable/lazy/atomic/other-property-behavior key paths as being refinements of that root protocol.

12 Likes

The overloads in standard library for key paths is an option, however the use-case goes beyond standard library. There are other libraries such as ReactiveSwift, RxSwift etc that suffering because of the similar problem. Providing overloads all over the Swift libraries is kinda a big ask and a huge burden on external Swift developers in terms of development and life-time maintenance of this.

2 Likes

This seems the most elegant solution to me by far. I make liberal use of KeyPaths and this idea of the . backslash syntax effectively becoming an alias for a function makes a lot of sense intuitively too.

I would really love for this to be adopted, particularly without needing an operator to "upcast" the key path.

If we can't/don't get to a true subtype relationship, Joe's suggestion to make the \. syntax work in a function context would get us 90% of the way there, with perhaps an operator or global function to perform the conversion if you're working with a concrete key path. I think that approach would actually fit pretty well with Swift's use of literals, which are a bit malleable and can become what you need in different contexts, versus instances, which need explicit conversion.

4 Likes

This makes a lot of sense to me. Perhaps we could eventually have an ExpressibleByKeyPathLiteral protocol which -> could conditionally conform to if / when structural types are able to conform to protocols. The use case of generic code I mentioned above could be better served in that future by introducing a Callable protocol backing the application syntax and to which functions, key paths and other user-defined types can all conform.

9 Likes