Why KeyPath?

I ought to have asked this question years ago, but I just keep hoping I'd run into a situation and the keypath would be the obvious solution, and I'd finally understand them.

So, I get that a KeyPath can store a fully fleshed out member chain to refer to a particular value somewhere in a data structure. And I see that people seem to want to use them, so I figure I'm just missing something.

What particular problem do KeyPaths solve? What's the alternative?

1 Like

Are you familiar with SwiftUI Bindings? I bet it's the most common use case now, though SwiftUI came out two years after key paths did:

Otherwise, they mostly just make closures that return a property look nicer.

There are other use cases, but I think you need to appreciate these first, before moving on to those.

4 Likes

The clearest use-case to me is in declaring comparators, esp. for deeply-nested structures (which handle optionals automatically). e.g.,

/// declaration
public static let comp = KeyPathCompares.c4(
    \Self.examId.area,
    \Self.grade,
    \Self.severity,
    \Self.label
  )

// boilerplate
  public static func < (lhs: GradedFinding, rhs: GradedFinding) -> Bool {
    comp.cmp(lhs, rhs) ?? false
  }

  public static func == (lhs: GradedFinding, rhs: GradedFinding) -> Bool {
    nil == comp.cmp(lhs, rhs)
  }

One under-appreciated benefit (and why I sometimes leave them public) is that when you as an API provider need to change your data structures, you don't have to change all the client code, so long as the clients consume your comparators instead of reaching into your structures directly.

This notion bloomed in late 1990's as "structure-shy" programming (out of Northeastern) and the "Law of Demeter" (know friends, but not friends-of-friends). In this the client delegates to the structure/model provider the task of traversing data. This avoid tangling clients in details they don't care about, and makes it easier for the provider to change structures.

So, this is what you'd do to avoid needing to pass a pointer for a deep member out?

I like the idea of being to change structures without screwing up the interface. But isn't that served by simply keeping your basic data members private and providing getters, setters, and conforming to protocols like Equatable and Comparable?

I remember 249 now. It felt very uncompelling. ".email" as a shortcut for "$0.email" is a lose, in that $0 is a well know signal of a special input, and in particular which one, in case there are several. I've no idea how keyPaths would refer to $1; positionally, I suppose.

keypaths were once very heavyweight and weird, but over time theyā€™ve evolved into a shorthand for (Root) -> Value closure literals. so the reason they donā€™t support $1 is because they are a shorthand - if you want to refer to $1, you can just write an actual closure literal.

if i recall correctly, KeyPath<Root, Value> is its own class type and not a typealias for (Root) -> Value because it was once thought that they would be ā€œfasterā€ than arbitrary function types.

It can be more than (Root) -> Value, like simultaneously being akin to (Root, Value) -> Void.

ReferenceWritableKeyPath makes SwiftUI more usable as well.

1 Like

Keypaths are not just a shorthand for passing a function value though. They support hashing and equality, and can be printed as a string. They can also be writable.

3 Likes

thatā€™s fair, iā€™m mostly going off my own habits where everything gets refactored into functions eventually. so perhaps iā€™m just not getting the most out of my keypaths.

however this is interesting to me:

i remember a long time ago i tried to use keypaths for carrying path names and gave up because i realized keypaths canā€™t produce strings. so i tried it again and i got

$ swift -e "print(\Int.bigEndian)"
\Int.<computed 0x00007f93be6461c0 (Int)>
$ swift -e "dump(\Int.bigEndian)"
- \Int.<computed 0x00007f0882954180 (Int)> #0
  - super: Swift.PartialKeyPath<Swift.Int>
    ā–æ super: Swift.AnyKeyPath
      - _kvcKeyPathStringPtr: nil
$ 

what am i missing?

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?