Why KeyPath?

Int.bigEndian is a computed property and key path stores a function reference to this getter, it does not store the name of its components.

2 Likes

ah, i see, i tried it with a custom type and it worked!

$ swift -e "struct Root { var x:Int } ; print(\Root.x)"
\Root.x

that’s really neat. is there a way to get the syntax components of the key path? can you check at runtime if a key path refers to a stored property?

Even if the keypath involves computed properties, it would be helpful for debug builds to store a useful printable version of the keypath.

3 Likes

I stumbled upon an answer on reddit a while back and I like it.

It's a form of indirection. You may want to refer to the variable itself, not the value inside it. For example an API like "notify me when view.frame changes". In that case you'd need to pass a KeyPath to your notification API.

I doubt if it's right to understand keypath as a closure. Just because it can be used as a shortcut for closure doesn't mean it's. Below is from SE-0294:

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

In the translated code, the function behavior comes from subscript, not keypath. Keypath serves as an identifier.

I suppose it's just an example to demonstrate keypath usage and doesn't necessarily mean it's the only or best approach for that scenario. I have another (trivial but useful) example here. In general features like keypath helps one to write more abstract code.

1 Like

I always assumed KeyPaths were added to Swift because they are such a common pattern in Apple's SDKs. When using Cocoa, UIKit, and Foundation it is a very common task to observe values with KVO and it was a clunky process to do without native Swift support.

isn’t { $0[keyPath: \User.email] } always equivalent to { $0.email }?

A keypath like \.email isn't a particularly interesting example. But it's perhaps worth noting that { $0[keyPath: \.i] } is not always semantically equivalent to { $0.i } and \.i (in a function context):

struct M {
    var i: Int = 123

    subscript<Value>(keyPath keyPath: KeyPath<M, Value>) -> Value {
        456 as! Value
    }
}

func go(_ f: (M) -> Int) {
    print(f(M()))
}

go(\.i)                  // prints 123
go({ $0.i })             // prints 123
go({ $0[keyPath: \.i] }) // prints 456

So I think the implementation is actually different than what SE-0249 specifies.

The more practical difference between a keypath literal (in a function context) and a closure is that the expressions inside subscripts in the keypath are evaluated once. Thus:

func go2(_ f: ([Int]) -> Int) {
    let a = [2, 3, 5]
    print(f(a) == f(a))
}

go2(\.[Int.random(in: 0..<3)])     // always prints true
go2({ $0[Int.random(in: 0..<3)] }) // sometimes prints false
1 Like

In addition to what @mayoff said, it seems that your understanding of keypath is mainly based on SE-0249. In my understanding SE-0249 is an application of keypath, not what it's. If there wasn't SE-0249, keypath would still be a useful feature.

It can't be a method. It specifically has to be an instance subscript. (callAsFunction is not even an option. :crying_cat_face:) I consider this "baking the input of a subscript, so that it becomes a property".

public extension MutableCollection where Index: Hashable {
  static func bakedSubscript(_ index: Index) -> WritableKeyPath<Self, Element> {
    \.[index]
  }
}

let stringArrayElement1 = [String].bakedSubscript(1) // Same as \[String].[1]; this just illustrates the idea with a name.
var array = ["👌", "☝️", "✌️"]
array[keyPath: stringArrayElement1] = "1️⃣"
1 Like

yeah, you are right. i personally did not use key paths a lot until SE-249, and now i realize i have been vastly under-utilizing this feature.

to sum it up, the read-only key paths:

  • have identities
  • can produce lexical string paths (but there doesn’t seem to be a straightforward way to determine at run-time if an arbitrary key path has one)
  • can capture subscript parameters

and the writable ones can mediate access to a subscript set or _modify in a way that cannot be easily expressed with a closure literal. have i missed anything?

it would be really interesting if we could create key paths to non-throwing functions. is there anything preventing us from enabling that?

Thanks. I misunderstood Rob's example code. I realized it and delete my question before I saw your reply.

As nominal types, key paths can be extended with protocol conformances, methods, and properties. Functions cannot be so extended. I haven't had an occasion to use that ability though (beyond the built-in conformances).

2 Likes

A fun thing I contemplated back in the day is "what if key paths could get conditional conformances based on attributes* of the properties along the path?". I think this might boil down to accidentally making a generalized effects system again, but it was a cute idea.

*for whatever definition of attributes.

2 Likes

That should be possible now that we do support protocol compositions with base classes. WritableKeyPath is already burned in as a subclass, it would definitely be more composable to introduce new capabilities like KeyPath<T, U> & Sendable & Codable by protocol composition rather than by extending the class hierarchy. We also have more generalized existentials than we used to, so at some point it might be nice to supersede the KeyPath class itself with a protocol now that we can express types like any KeyPathProtocol<T, U>.

12 Likes

So, trying to boil this down to an elevator pitch on KeyPaths. What I have so far is:

KeyPaths allow you to hand over a reference to a member of a type, as opposed to the value of that member at the present time, which can be useful for when something need to know not only what value to work with, but also what member of that value is important. For instance, if you have a structure with several members, a KeyPath can be used to say which member ought to be used to sort with.

So, given:

struct Worker {
  var name: String
  var title: String
  var division: String
}

func consider(worker: F, workerData: KeyPath<F, String>) {
    print(worker[keyPath: workerData])
}

let x = F(name: "Jack", title: "Tradesman", division: "Mechs")

consider(follower: x, followerItem: \.division) // prints "Mechs"

Okay, that much makes sense to me.

1 Like

does KeyPath still use reference counting?

despite knowing exactly what AnyObject-constrained existentials are, for some reason my brain still thinks of the any keyword as “slower” than a polymorphic class instance. old prejudices die hard…

Another benefit of a protocol is that you could define types and functions generic over the protocol in order to store the information for a particular keypath inline without wrapping.

I use keypaths with map every single day, much clearer than using closures.

1 Like

While "clearer" doesn't represent how I feel about \., $ is the most nonsense-looking, American-capitalism-touting, rump-ugliest thing in the language. It was bad enough with $0—at least that could be avoided by supplying a name. Property wrappers + SwiftUI made it impossible.
:money_mouth_face:

1 Like

Yeah, having suffered through the hardworking weirdness that is Perl for years as a sys-admin, I’m not a big fan of the use of sigils. This applies to Optionals as well. I’m used to it, but that’s as far as I’ll go.