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?
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.
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.
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.
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
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.
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.
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
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. ) 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ļøā£"
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?