1
votes

Imagine I have a struct AStruct:

struct AStruct {
    let name: String
    let value: Int
}

And imagine I have an array of AStructs:

let array: [AStruct] = [
AStruct(name: "John", value: 1),
    AStruct(name: "Bob", value: 23),
    AStruct(name: "Carol", value: 17),
    AStruct(name: "Ted", value: 9),
    AStruct(name: "Alice", value: 13),
    AStruct(name: "Digby", value: 4)
]

Now imagine I want to use a map() to create an array of integers out of my array of AStructs.

This code works:

let values = array.map { $0.value }

But if I try to use Swift key path syntax I get errors no matter what I try. Example:

let values = array.map { \.value }

I get an error:

Cannot infer key path type from context; consider explicitly specifying a root type.

So how do I that?

I can get it to work if I give the input a name and type:

let values = array.map { (aVal: AStruct) -> Int in
    return aVal.value
}

But that is distinctly not key path syntax.

2
You pass the key path as the parameter instead of a closure: let values = array.map(\.value).TylerP
Ok, that works, but why doesn't it work with closure syntax? (This is a simplified case to illustrate the problem. It isn't always as simple as passing a key path in to map().)Duncan C

2 Answers

2
votes

SE-0249: Key Path Expressions as Functions is the relevant Swift Evolution proposal for this feature, and from that proposal:

As implemented in apple/swift#19448, occurrences of \Root.value are implicitly converted to key path applications of { $0[keyPath: \Root.value] } wherever (Root) -> Value functions are expected. For example:

users.map(\.email)

Is equivalent to:

users.map { $0[keyPath: \User.email] }

<snip>

When inferring the type of a key path literal expression like \Root.value, the type checker will prefer KeyPath<Root, Value> or one of its subtypes, but will also allow (Root) -> Value. If it chooses (Root) -> Value, the compiler will generate a closure with semantics equivalent to capturing the key path and applying it to the Root argument. For example:

// You write this:
let f: (User) -> String = \User.email

// The compiler generates something like this:
let f: (User) -> String = { kp in { root in root[keyPath: kp] } }(\User.email)

In other words: the compiler recognizes a keypath expression when passed in as a direct argument to where a function is expected, and injects a conversion from the keypath to a generated closure. When you write

array.map { \.value }

you are passing in a closure whose body evaluates to a key path, and because the key path is then seemingly arbitrary to the compiler here (e.g., it has no context for what type \.value is rooted off of), you get the error.


Also important to note is that:

The implementation is limited to key path literal expressions (for now), which means the following is not allowed:

let kp = \User.email // KeyPath<User, String>
users.map(kp)

I don't recall whether this restriction has been lifted at this point, but if you find yourself in a situation where you have a keypath you need to apply but automatic conversion won't work, you'll need to pass the keypath into a closure manually, and key off of that keypath directly.

1
votes

SE-0249 allows for key paths to be treated as closures. map takes an "(Element) throws -> T"—for example, in your case, this one:

{ $0.value } as (AStruct) -> _

But a key path is not a closure. So you can't treat it as one…

array.map { (\.value)($0) }

…unless you explicitly type it:

array.map { (\.value as (_) -> _)($0) }

But you would never do that. The original

array.map { $0.value }

syntax is better. However, the most common way to cast the key path to a closure is better-looking.

array.map(\.value)

Also, key paths currently have a feature related to implicitly-opened existentials that functions do not.