Video #8: Getters and Key Paths
Episode: Video #8 Date: Mar 19, 2018 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep8-getters-and-key-paths

Description
Key paths aren’t just for setting. They also assist in getting values inside nested structures in a composable way. This can be powerful, allowing us to make the Swift standard library more expressive with no boilerplate.
Video
Cloudflare Stream video ID: 6e59bc706e0ebc05917d968cc5e38845 Local file: video_8_getters-and-key-paths.mp4 *(download with --video 8)*
References
- Discussions
- SE-0249: Key Path Expressions as Functions
- 0008-getters-and-key-paths
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
We’ve spent the last two episodes diving deep into the world of functional setters and we’ve seen how they allow us to manipulate large data structures with precision and composition. This is only half of the picture! What about getters? Let’s explore how we access data from our structures, explore how getters compose, and see how key paths may further aid us along the way! Properties
— 0:47
Classes and structs typically store their data in properties, which are accessed much like methods using dot-syntax, but where methods must be called, properties immediately return their values. struct User { let id: Int let email: String }
— 1:08
We can instantiate a user and access its properties using dot syntax. let user = User(id: 1, email: " [email protected] ") user.id // 1 user.email // " [email protected] "
— 1:22
Properties act like methods that take zero arguments, which in turn act like functions that take one argument, self being that otherwise the implicit argument. This is kind of amazing, because it’s our favorite shape for function composition!
— 1:45
Of course, they don’t compose well out-of-the-box, but short closures make this pretty easy.
— 2:00
Like what if we wanted to extract the id from a user and convert it to a string: { (user: User) in user.id } >>> String.init // (User) -> String
— 2:27
This works but is a bit noisy. Swift gives us a much more powerful language feature we can use instead. Key paths as getters
— 2:41
Swift has a powerful, compiler-generated language feature that allow us to refer to property access without an instance: key paths. The syntax looks a little funny. \User.id // KeyPath<User, Int> Here we have a KeyPath that’s generic over the root container type, User and the type of value being accessed, in this case an Int .
— 3:02
So what can we do with these key paths? Well, we can use them to access a property on a value. user[keyPath: \User.id] // 1
— 3:20
This doesn’t seem super useful when we have dot-syntax. user.id // 1 But as we’ve seen in the past, the ability to refer to this kind of functionality in the abstract unlocks all kinds of compositions. Beyond that, it’s compiler-generated code that Swift gives us for free! We should be able to take advantage of this to simplify our earlier attempt of composition: { (user: User) in user.id } >>> String.init
— 4:05
The lefthand side is just a function that takes a User and returns the value of a property on that user, which is precisely the functionality key path getters give us.
— 4:20
Let’s define a function that, given a key path, produces a getter function. func get<Root, Value>(_ kp: KeyPath<Root, Value>) -> (Root) -> Value { return { root in root[keyPath: kp] } }
— 4:58
How do we use this? get(\User.id) // (User) -> Int We get a brand new function from User to Int for free! That’s pretty neat. This is exactly what we needed for composition earlier. get(\User.id) >>> String.init // (User) -> String It’s even shorter!
— 5:44
Swift also has the notion of “computed” properties, which are properties that don’t store data directly, but rather store logic. Computed properties are really just functions in disguise, but they give us the ability to hide private implementation details, unify getter–setter function pairs with a single construct, and contain less visual noise at the call site. There’s no compiler-enforced rule as to when it’s appropriate or not to use a computed property instead of a method, but the general feeling is that property access should be inexpensive and predictable for a given state.
— 6:23
Let’s add a computed property to our user. extension User { var isStaff: Bool { return self.email.hasSuffix("@pointfree.co") } } It contains some logic to check that an email has a certain suffix. user.isStaff // true
— 6:38
Swift generates key paths for these computed properties, as well! \User.isStaff // KeyPath<User, Bool>
— 6:50
We can use it the same way as before. user[keyPath: \User.isStaff] // true
— 6:55
But of course, this isn’t the practical way of using key paths. Our get function makes it immediately useful, though. get(\User.isStaff) // (User) -> Bool
— 7:07
Since getters can be thought of as functions that focus on a part of a larger structure, they can be thought of as a very basic transformation function. There’s a higher-order method in the standard library that takes these transformation functions that may compose nicely with key paths: map . For example, let’s say we have an array of users. let users = [ User(id: 1, email: " [email protected] "), User(id: 2, email: " [email protected] "), User(id: 3, email: " [email protected] "), User(id: 4, email: " [email protected] ") ]
— 7:21
We can map over it with a closure to produce an array of email addresses. users .map { $0.email } Seeing how key paths work, we might expect this to work: users .map(\User.email) Unfortunately, the language doesn’t yet support converting key paths to getter functions, and the standard library doesn’t define a map overload that accepts a key path.
— 7:50
We could define an overload ourselves. Traversing into a property is a pretty common operation for map . extension Sequence { func map<Value>(_ kp: KeyPath<Element, Value>) -> [Value] { return self.map { $0[keyPath: kp] } } }
— 8:26
Now we can pass a key path instead! users .map(\User.email)
— 8:31
If we leverage type inference, we have something that’s even shorter than our point-full version. users .map(\.email)
— 8:34
This looks pretty nice! Now we probably also want to write something like this: users .filter(\.isStaff)
— 8:44
But we need another overload. This isn’t scaling very well. Do we really want to define an overload for every relevant higher-order function in the standard library? And what about other third-party code?
— 9:23
Luckily, we’ve already defined a bridge ourselves! We can use get in all of these cases.
— 9:38
Instead of relying on our ad hoc overload, we can use get with the map that ships with Swift. users .map(get(\.email))
— 9:44
And instead of relying on a key path overload for filter , we can pass get to the standard library method instead. users .filter(get(\.isStaff))
— 9:53
We don’t have to define any overloads. We can use key paths with all of these APIs for free. And now that we’re lifting key paths into the function world, we can even compose them together.
— 10:03
Let’s revisit a property of map . We saw that we can map over an array twice: users .map(get(\.email)) .map(get(\.count)) // [17, 33, 13, 26]
— 10:19
We saw that this could be rewritten with a single map and composition. users .map(get(\.email) >>> get(\.count)) // [17, 33, 13, 26] It’s a nice performance gain, too! A single traversal instead of two.
— 10:33
Key paths compose themselves, though! We don’t need the arrow operator here. users .map(get(\.email.count)) // [17, 33, 13, 26]
— 10:57
Getter composition unlocks a lot of nice things. We can take our isStaff getter and invert it by composing with the ! operator. users .filter(get(\.isStaff) >>> (!))
— 11:16
Using a prefix operator in this order may read a bit strangely, but luckily we have backwards composition to fix this! users .filter((!) <<< get(\.isStaff))
— 11:43
It’s amazing how we can take these units and plug them into map and filter so easily, and how we’re free to compose these getter transformations with other functions! Sorting
— 12:07
What are some other higher-order functions that might benefit from key path composition? How about sorting? It’s common to want to sort by a property, but Swift doesn’t really help us here, out of the box. We’re invariably pulled to the point-full world. user .sorted(by: { $0.email < $1.email })
— 12:45
This isn’t bad, but we can imagine that sorting on a deeply-nested properties becomes a bit noisy. users .sorted(by: { $0.email.count < $1.email.count })
— 13:04
This is becoming verbose. We have to repeat ourselves on each side depending on the nested property we’re sorting on. That property is just a key path, though, so maybe we can find another bridge from the key path world do the function sorted expects.
— 13:32
What’s sorted look like, exactly? users.sorted(by: <#(User, User) throws -> Bool#>) It takes a very specific function: one that takes a pair of elements and returns whether or not one should appear before the other.
— 13:44
We should be able to produce functions of this type but dive deeper into User and sort on a nested property. Let’s do just that. func their<Root, Value>( _ f: @escaping (Root) -> Value, _ g: @escaping (Value, Value) -> Bool ) -> (Root, Root) -> Bool { return { g(f($0), f($1)) } } There’s quite a bit going on here, but the main thing to note is that we can apply our transformation from Root to Value to both of our roots independently. This allows us to create a brand new function that cares about Value s.
— 14:53
How does it look like to use this function? users .sorted(by: their(get(\.email), <))
— 15:14
Not bad! We can invert our sort with a single character. users .sorted(by: their(get(\.email), >))
— 15:20
We can also dive in deeper, and sort by the character count of a user’s email address. users .sorted(by: their(get(\.email.count), >))
— 15:31
Not bad! We managed to avoid having to define an overload. Our combinator plugs nicely into an existing method. And we’ve built something that works with any function, not just key paths! What are some other methods that may benefit? How about max ? It takes a function (User, User) -> Bool here. users .max(by: their(get(\.email), <))
— 16:02
We can do the same with min ! users .min(by: their(get(\.email), <))
— 16:14
It’s admittedly a bit strange to specify < given that it’s the only valid way of getting the maximum or minimum. Maybe we can define an overload to help. func their<Root, Value: Comparable>( _ f: @escaping (Root) -> Value ) -> (Root, Root) -> Bool { return their(f, <) } By constraining Value to Comparable , we can define a default version of their for comparable types.
— 16:50
Now our max and min don’t have to worry about that extra argument. users .max(by: their(get(\.email))) users .min(by: their(get(\.email))) This helps with sorted , too, where our default is probably ascending.
— 17:08
Not bad! We took three standard library methods and were able to define one that bridged things to the world of key paths. Reduce
— 17:58
Let’s look at one more higher-order function: reduce . It’s an interesting function that takes a starting value and then folds data into it by passing each element through a transformation function. [1, 2, 3] .reduce(0, +) // 6
— 18:31
What if our data structures are more complicated? What if we want to reduce data based on deeply nested properties of our data? For example: struct Episode { let title: String let viewCount: Int } let episodes = [ Episode(title: "Functions", viewCount: 961), Episode(title: "Side Effects", viewCount: 841), Episode(title: "UIKit Styling with Functions", viewCount: 1089), Episode(title: "Algebraic Data Types", viewCount: 729), ] If we wanted to sum the view count of all of these episodes, we could write the following code: episodes .reduce(0) { $0 + $1.viewCount } This is pretty short but relies heavily on our ability to remember what $0 and $1 refer to at a glance.
— 19:23
Let’s focus on the shapes and see if we can write a function to bridge these worlds and make it a little clearer how we can accumulate based on substructures of larger values. func combining<Root, Value>( _ f: @escaping (Root) -> Value, by g: @escaping (Value, Value) -> Value ) -> (Value, Root) -> Value { return { value, root in g(value, f(root)) } } }
— 20:43
How do we use this? episodes.reduce(0, combining(get(\.viewCount), by: +)) This is a bit more verbose, but it’s arguably more readable and built from small, composable units. Operator overload?
— 21:29
We’re seeing the benefit of get in a lot of places, and I’m sure we’ll see more! It’s probably sensible to accept the extra bit of noise here, but…we haven’t introduced an operator in awhile, and I think we could try something out to make this process a bit more lightweight. prefix operator ^ prefix func ^ <Root, Value>(kp: KeyPath<Root, Value>) -> (Root) -> Value { return get(kp) } A nice thing about prefix operators is we don’t have to worry about precedence or associativity!
— 22:22
How do we use this operator? ^\User.id // (User) -> Int There’s our getter! We can pass it directly to a map . users.map(^\.id) // [1, 2, 3, 4]
— 22:32
This looks nice! And very close to the original map overload we defined. What about our other examples?
— 22:45
We can map over the character counts of our users’ emails. users.map(^\.email.count) // [17, 33, 13, 26] We still have the ability to forward-compose these functions into new functions. users.map(^\.email.count >>> String.init) // ["17", "33", "13", "26"] We can filter without a lot of ceremony. users.filter(^\.isStaff) // or users.filter((!) <<< ^\.isStaff)
— 23:27
This isn’t magic! It’s just function composition. The ! operator is merely a function that takes a single boolean argument and negates it. It’s pretty nice that it works this way.
— 23:43
What about our bridges from earlier? We could sort by a user’s email. users.sorted(by: their(^\.email)) In a descending order, even. users.sorted(by: their(^\.email, >))
— 23:57
Our max and min work just as before. users.max(by: their(^\.email.count)) users.min(by: their(^\.email.count)) All of these examples zoom right in on what they care about. The get function is pretty short, but it seems to be common enough to avoid with an operator if we can.
— 24:24
We’ve introduced a new operator, though. Does it tick our boxes? Unlike our previous operators, this one already exists in Swift, but as an infix, not prefix, operator. As a prefix operator, it seems unlikely to cause too much confusion, but it’s definitely overlapping with existing symbol space. Does it have prior art and a nice shape? Well, the shape is nice, it’s kind of like an arrow pointing up, as if it’s lifting a key path upward into the function world. The shape also evokes Objective-C block syntax, which are functions themselves. Do other languages have similar features? Ruby uses prefix & to do something similar, but we can’t use the same symbol because prefix & is reserved for inout call sites in Swift. Is it solving a universal problem? Well, property access is everywhere in Swift, so yes. It doesn’t check the boxes quite so solidly, but depending on how often you use it, it may be a valuable addition to reduce parentheses and noise. What’s the point?
— 26:38
We introduced a get function that only works with KeyPath values, something we don’t deal with very often on a daily basis. Sometimes it made things a bit shorter. Sometimes it didn’t! Should we care about this kind of thing? We think so! And we think it helps show how powerful key paths are, and why compiler-generated code is, as well! Key paths are a somewhat foreign addition to our everyday code, syntax and all, but let’s focus on what they buy us. This entire episode is basically a celebration of a single, simple function: get . All it did was lift a key path over to the function world. This allowed us to immediately work with a lot of other library code, though, starting with map and filter . Any time the compiler can work for us, it’s less work for us to do. Let’s embrace these occasions! References SE-0249: Key Path Expressions as Functions Stephen Celis & Greg Titus • Mar 19, 2019 A proposal has been accepted in the Swift evolution process that would allow key paths to be automatically promoted to getter functions. This would allow using key paths in much the same way you would use functions, but perhaps more succinctly: users.map(\.name) . https://forums.swift.org/t/se-0249-key-path-expressions-as-functions/21780 Downloads Sample code 0008-getters-and-key-paths Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .