Video #7: Setters and Key Paths
Episode: Video #7 Date: Mar 12, 2018 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep7-setters-and-key-paths

Description
This week we explore how functional setters can be used with the types we build and use everyday. It turns out that Swift generates a whole set of functional setters for you to use, but it can be hard to see just how powerful they are without a little help.
Video
Cloudflare Stream video ID: 9e0a45c8e6ec68f2426fe80a71f92b8c Local file: video_7_setters-and-key-paths.mp4 *(download with --video 7)*
References
- Discussions
- open sourced for people to poke at
- Composable Setters
- Functional Swift Conference
- Semantic editor combinators
- 0007-setters-and-key-paths
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- MIT License
Transcript
— 0:06
In the last episode we explored how functional setters allow us to dive deeper into nested structures to perform transformations while leaving everything else in the structure fixed. We played around with some toy examples, like nested tuples and arrays, and we showed off some pretty impressive stuff, but at the end of the day we aren’t typically transforming tuples. Instead, we have real world data with structs. We want to bring all the ideas from the previous episode into the world of structs so that we can transform a deeply nested struct in a simple, expressive manner. To do this, we are going to leverage Swift’s key paths! Structs Here we have a few related structs: struct Food { var name: String } struct Location { var name: String } struct User { var favoriteFoods: [Food] var location: Location var name: String } let user = User( favoriteFoods: [ Food(name: "Tacos"), Food(name: "Nachos") ], location: Location(name: "Brooklyn"), name: "Blob" )
— 1:20
We have a Food struct, a Location struct, and a User struct, which holds onto some food, a location, and a name. We also have a sample user to play around with.
— 1:34
What if we wanted to dive into the user’s location’s name and relocate them to Los Angeles and produce a whole new user value? The most naive approach would looks something like this: User( favoriteFoods: user.favoriteFoods, location: Location(name: "Los Angeles"), name: user.name ) We created a whole new user by leaving most values fixed, but providing a new location with an updated name.
— 2:02
This is a nice way of creating a new value and transforming one small part, but as we saw in our last episode on tuples, we saw that there was a better way than to build every new value from scratch every time.
— 2:21
Here’s a function from last time, first , whose sole purpose was to transform the first component of a tuple: func first<A, B, C>(_ f: @escaping (A) -> B) -> ((A, C)) -> (B, C) { return { pair in (f(pair.0), pair.1) } }
— 2:33
Let’s replace these generics with some concrete types that express how to transform a user’s location name: func userLocationName(_ f: @escaping (String) -> String) -> (User) -> User { return { user in User( favoriteFoods: user.favoriteFoods, location: Location(name: f(user.location.name)), name: user.name ) } }
— 3:16
OK, lots of boilerplate, but it does at least lift the setter on location into the world of functions. We can use it as such: user |> userLocationName { _ in "Los Angeles" } // "Los Angeles" user |> userLocationName { $0 + "!" } // "Brooklyn!"
— 3:42
OK, and that looks nice because it’s starting to resemble what we did for tuples. The downside though is that we need to cook up these setter functions for every field of every type we have. It’s not generic at all!
— 4:00
What’s more: if we ever make changes to the fields of any of these structs (adding, removing, renaming), it will break all of our existing setters. Key paths
— 4:11
Luckily, we can leverage a wonderful Swift feature to make this so much better: key paths!
— 4:24
Key paths are compiler generated code that provide us a functional getter and setter for every field of a struct. It’s just that their syntax is a lil strange, so it’s hard to see how nice they can be.
— 4:54
To get access to the key path, you use this special syntax: \User.name // KeyPath<User, String> This gives us a key path from a user to a string. The first generic is called the “root” of the key path, and the second is called the “value.”
— 5:12
We can use this key path in two ways. One way is to extract the name from a user. user[keyPath: \User.name] // "Blob" Neat, but why would we ever use this when property dot-syntax is at hand? user.name // "Blob"
— 5:38
We can also use this key path to set the name in the user: var copy = user copy[keyPath: \User.name] = "Blobbo"
— 5:50
When key paths have setters, this means they are a subclass of KeyPath called WritableKeyPath .
— 6:02
Also neat, but why should we care when property dot-syntax is much simpler? copy.name = "Blobbo"
— 6:17
This kind of key path usage isn’t an interesting one. What is interesting is that Swift gave us a compiler generated value that we can use to get and set properties on a struct. This means we can completely separate the concept of a data from the operation that changes it. The dot syntax is not capable of doing this. Compiler-generated setters
— 6:36
To make this explicit, let’s define a helper that lifts a writable key path up into a world that transforms the root of the key path. We’re even going to generalize this a bit by allowing us to supply a function that further transforms the value before plugging it back into the root. func prop<Root, Value>(_ kp: WritableKeyPath<Root, Value>) -> (@escaping (Value) -> Value) -> (Root) -> Root { return { update in { root in var copy = root copy[keyPath: kp] = update(copy[keyPath: kp]) return copy } } }
— 7:28
If you squint you’ll notice that the signature of this prop function after the keyPath part is the same as the first and second functions we defined on tuples: given a transformation on the values we can get a transformation on the roots.
— 8:41
Just with this lil helper we can already use it with our User struct. prop(\User.name) // ((String) -> (String)) -> (User) -> User
— 9:04
We may be tempted to use trailing closure syntax to call our new function with a transformation. prop(\User.name) { _ in "Blobbo" } Extra argument in call
— 9:13
But this doesn’t work: Swift parses trailing closure syntax here as being the second argument to prop . We can use parentheses, though, to signal our intent. We could wrap our closure in parentheses, but the trailing syntax can be nicer to read. Luckily, we can also wrap our call to prop in parentheses and retain the trailing closure. (prop(\User.name)) { _ in "Blobbo" } // (User) -> User
— 9:25
Now we have a reusable function that takes a User as input and spits out a brand new User ! Renaming a user to “Blobbo” might not be the most reusable thing, though, how about we uppercase a user’s name instead? (prop(\User.name)) { $0.uppercased() }
— 9:45
What if we wanted to transform the user’s location name? As we saw last time , we can compose setters together using <<< . prop(\User.location) <<< prop(\Location.name) // ((String) -> String) -> (User) -> User
— 10:14
A nice thing about key paths, though, is they compose using dot-chaining! prop(\User.location.name) // ((String) -> String) -> (User) -> User
— 10:28
This function we’ve created is exactly the same as the userLocationName function we defined by hand earlier. This time, however, we were able to use compiler-generated code to do most of the work for us!
— 11:04
And now let’s transform our user value. user |> (prop(\User.name)) { $0.uppercased() } |> (prop(\User.location.name)) { _ in "Los Angeles" }
— 11:29
Because everything is just functions, this stuff already works with the first and second tuple transformations we defined earlier. No additional work is needed, let’s just compose the functions!
— 11:45
We can embed our user in a tuple and see this composition in action. (42, user) |> (second <<< prop(\User.name)) { $0.uppercased() }
— 12:00
This is nice, but let’s make it even nicer! Swift’s key paths allow for an abbreviated syntax wherever the type can be inferred. In our examples so far, we can remove a lot of explicit types: prop(\User.location) <<< prop(\.name) user |> (prop(\.name)) { $0.uppercased() } |> (prop(\.location.name)) { _ in "Los Angeles" } (42, user) |> (second <<< prop(\.name)) { $0.uppercased() }
— 12:13
This is looking pretty clean! Let’s play around with that last example a bit more. (42, user) |> (second <<< prop(\.name)) { $0.uppercased() } |> first(incr)
— 12:22
This stuff is really powerful! Our previous examples using tuples didn’t feel like they applied to code we write every day, but now we’re seeing composable setters also work nicely with the types that we define. We’re still using tuples here, so how about a more common data structure? Let’s work with an array.
— 12:42
Inside the User struct there is an array of favorite foods. user.favoriteFoods // [Food(name: "Tacos"), Food(name: "Nachos")]
— 12:52
In last week’s episode , we used the free map function to traverse into an array and apply a transformation to each element. This composed with all of our other setters.
— 1:14
If we wanted to transform our user’s favorite foods in place, we could use the map method. user.favoriteFoods .map { Food(name: $0.name + " & Salad") } // [Food(name: "Tacos & Salad"), Food(name: "Nachos & Salad")]
— 13:29
This returns an array of foods, though, not a user, so we’re dealing with that problem of having to reconstruct one from scratch. Let’s try using our setters instead. prop(\User.favoriteFoods) <<< map <<< prop(\.name) // ((String) -> String) -> (User) -> User
— 14:08
We can use this setter with a reusable transformation. let healthier = (prop(\User.favoriteFoods) <<< map <<< prop(\.name)) { $0 + " & Salad" } // (User) -> User
— 14:25
Now we have a reusable function and we’re free to pipe data through! user |> healthier Now we have a user whose favorite foods all come with a side salad. Because these reusable functions deal with a single type, we can chain them along and apply the same transformation more than once. user |> healthier |> healthier
— 14:47
We can keep chaining these transformations along! user |> healthier |> healthier |> (prop(\.location.name)) { _ in "Miami" } // "Miami" |> (prop(\.name)) { "Healthy " + $0 } // "Healthy Blob" What happens if we nest this in another structure. Let’s tuple our user up! (42, user) |> second(healthier) |> second(healthier) |> (second <<< prop(\.location.name)) { _ in "Miami" } |> (second <<< prop(\.name)) { "Healthy " + $0 } |> first(incr) Other than our first line, we have a bunch of logic that can live on its own! Let’s use composition to build up a giant function. second(healthier) <> second(healthier) <> (second <<< prop(\.location.name)) { _ in "Miami" } <> (second <<< prop(\.name)) { "Healthy " + $0 } <> first(incr) We have all these transformations against the second element, but as we’ve seen in the past , setters distribute over composition, so we can use this to reshape our transformation into something simpler. second( healthier <> healthier <> (prop(\.location.name)) { _ in "Miami" } <> (prop(\.name)) { "Healthy " + $0 } ) <> first(incr)
— 17:08
There are other structures that we want to be able to traverse into. For example, if the User had an optional field, you may want to be able to safely traverse into that and perform a transformation. To do that you’d want to use the map on optional to safely traverse into it. But to do that you need a free map like we have for arrays: func map<A, B>(_ f: @escaping (A) -> B) -> ([A]) -> [B] { return { $0.map(f) } } We merely need to swap out the array for a question mark. func map<A, B>(_ f: @escaping (A) -> B) -> (A?) -> B? { return { $0.map(f) } }
— 17:43
It compiles, and is the exact functional setter we need to dive into optionals.
— 17:52
It doesn’t stop with optionals! We’ve covered the Either and Result types, before, which are basically just beefed-up versions of Optional , and so if you mixed in those kinds of values you probably also want a way to traverse into those structures and transform things.
— 18:12
Well, amazingly, all of that is possible, and it is very similar to what we have already done. We encourage the viewer to try that out in the exercises! What’s the point? Why are we going this far into wild, higher-order function composition land when Swift gives us value types? Value types are very nice model for immutable data and give us a very reasonable model for mutation. We can just create a copy, make mutations, and return the new value. var newUser = user newUser.name = "Blobbo" newUser.location.name = "Los Angeles" newUser.favoriteFoods = copy.favoriteFoods.map { Food(name: $0.name + " & Salad") } This is pretty nice! The last line is a bit noisy, but not too bad. Isn’t this good enough?
— 18:58
Swift’s faculties for mutating values and scoping mutation is really great. And for the common case where we make ad hoc transformations in one local area, it’s probably the way to go. But, as we saw, with more complex structures like arrays and enums, this kind of spirals out of control. Our ad hoc transformations start attracting more and more boilerplate, with a nested map here, an if case let there, and so on. Functional setters automate these cumbersome transformations into simpler, composable units that hide all that boilerplate.
— 19:29
Beyond that, comparing functional setters and mutable values leads to a powerful distinction between expressions and statements. A expression is a unit of code that evaluates to a value, while a statement is a unit of code that performs some action, with no return value.
— 19:51
In the case of this newUser mutation, we have a bunch of statements: one that describes how to make a copy of a user, and then a few steps that imperatively mutate it.
— 19:59
For a simpler example of this distinction, let’s take an array of integers. let xs = [1, 2, 3] Now let’s transform it in two different ways, once as an expression and once using statements. xs.map { $0 + 1 } var ys = Int xs.forEach { x in ys.append(x + 1) }
— 20:27
The first is an expression, in that we are evaluating it for its computed value, not for the actions that take place in computing it. We didn’t even need to assign it to a variable. Whereas the second is composed of statements that are executed in order to get their effects into the world. The first style better expresses the intent of the code than the second.
— 20:40
These functional setters allow us to continue living in this expression world. We don’t have to create lots of temporary, throwaway variables that we must mutate in statements to get the final value. We can do the whole transformation in a single expression. Configuring with expressions
— 20:56
Let’s look at an example from the codebase that powers this very site, which is completely open sourced for people to poke at . An atom XML feed expects dates in a particular format, so we want to create a date formatter from their specification. We can do this in a single expression using functional setters: let atomDateFormatter = DateFormatter() |> (prop(\.dateFormat)) { _ in "yyyy-MM-dd'T'HH:mm:ssZZZZZ" } |> (prop(\.locale)) { _ in Locale(identifier: "en_US_POSIX") } |> (prop(\.timeZone)) { _ in TimeZone(secondsFromGMT: 0) } This is not even possible in the statement world because this is a top-level variable, and most of these statements are not allowed at the top level. let atomDateFormatter = DateFormatter() atomDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" atomDateFormatter.locale = Locale(identifier: "en_US_POSIX") atomDateFormatter.timeZone = TimeZone(secondsFromGMT: 0) // Expressions are not allowed at the top level
— 21:43
So what we have to do is bundle this code up into a closure that can be executed: let atomDateFormatter: DateFormatter = { let atomDateFormatter = DateFormatter() atomDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" atomDateFormatter.locale = Locale(identifier: "en_US_POSIX") atomDateFormatter.timeZone = TimeZone(secondsFromGMT: 0) return atomDateFormatter }()
— 22:03
But now we’ve added a lot of ornamentation to what should be a very simple value configuration. Composing with expressions
— 22:17
There are other examples of where we do this kind of configuration of a value. For example, creating and preparing a URLRequest : var request = URLRequest( url: URL(string: "https://www.pointfree.co/hello")! ) request.allHTTPHeaderFields?["Authorization"] = "Token deadbeef" request.allHTTPHeaderFields?["Content-Type"] = "application/json; charset=utf-8" request.httpMethod = "POST" Here we’re doing a common task: configuring a URLRequest in order to fetch data off a server somewhere.
— 22:43
Let’s inspect those headers. request.allHTTPHeaderFields // [:] This is strange! Why are the header fields empty when we set them? We assigned using optional-chaining because the allHTTPHeaderFields property is optional, but we have an empty dictionary here, not nil .
— 22:55
This property apparently starts out as nil , but as soon as we set the httpMethod , our request is assigned an empty dictionary of headers. var request = URLRequest( url: URL(string: "https://www.pointfree.co/hello")! ) request.allHTTPHeaderFields?["Authorization"] = "Token deadbeef" request.allHTTPHeaderFields?["Content-Type"] = "application/json; charset=utf-8" request.allHTTPHeaderFields // nil request.httpMethod = "POST" request.allHTTPHeaderFields // [:]
— 23:07
We’ve just listed a bunch of statements describing how to create and configure a request, but the HTTP header fields property is nil to begin with, so those changes didn’t actually stick. What we probably want to do is initialize the headers dictionary explicitly: request.allHTTPHeaderFields = [:]
— 23:11
And now our headers are set as expected. It’s a bit unfortunate, though, and we’ll have to remember to do this every time we work with URLRequest s.
— 23:18
Our current solution is still far from perfect. By assigning a brand new empty dictionary, we might accidentally erase any previous header field configuration. What we really want to do is assign an empty dictionary if (and only if) there is no current dictionary. request.allHTTPHeaderFields = request.allHTTPHeaderFields ?? [:]
— 23:27
We’re starting to build up a lot of boilerplate, and we have to remember to do this every time. Instead, we could cook up lots of little setter helpers that can be composed together.
— 23:48
First, we should have one that guarantees the headers property holds an actual dictionary. let guaranteeHeaders = (prop(\URLRequest.allHTTPHeaderFields)) { $0 ?? [:] }
— 24:05
How can we use this? How about as the basis for a helper that configures a request to send POST bodies as JSON: let postJson = guaranteeHeaders <> (prop(\.httpMethod)) { _ in "POST" } <> ( prop(\.allHTTPHeaderFields) <<< map <<< prop(\.["Content-Type"]) ) { _ in "application/json; charset=utf-8" } This uses a feature we haven’t yet talk about: dictionary subscript key paths. We’ll have to cover that more in the future.
— 25:35
We also interact with the GitHub API, which requires a very specific Accept header, so let’s build a helper for that: let gitHubAccept = guaranteeHeaders <> ( prop(\.allHTTPHeaderFields) <<< map <<< \.["Accept"] ) { _ in "application/vnd.github.v3+json" }
— 26:18
We can also cook up a helper for attaching an authorization header: let attachAuthorization = { (token: String) in guaranteeHeaders <> ( prop(\.allHTTPHeaderFields) <<< map <<< prop(\.["Authorization"]) ) { _ in "Token \(token)" } }
— 26:59
Now we can create and configure a request in a single expression: URLRequest(url: URL(string: "https://www.pointfree.co/hello")!) |> attachAuthorization("deadbeef") |> gitHubAccept |> postJson
— 27:22
No intermediate statements or throwaway variables are needed. We can configure everything in a single expression. Testing with expressions
— 27:33
There’s yet another example, and that’s testing! When we write tests, we often want to create values of a very specific shape to be fed into our logic so that we can assert on what happens.
— 28:00
With functional setters we can create a bunch of helper functions that can be composed together for testing purposes. For example, we can create a bunch of setters that configure a user in a certain way: let noFavoriteFoods = prop(\User.favoriteFoods) { _ in [] } let healthyEater = prop(\User.favoriteFoods) { _ in [Food(name: "Kale"), Food(name: "Broccoli")] } let domestic = prop(\User.location.name) { _ in "Brooklyn" } let international = prop(\User.location.name) { _ in "Copenhagen" }
— 29:11
To use these, we still need to pipe a user through. We can create an unopinionated “template” value that we can use more generally. For example: extension User { static let template = User( favoriteFoods: [Food(name: "Tacos"), Food(name: "Nachos")], location: Location(name: "Brooklyn"), name: "Blob" ) }
— 29:36
Now we can take a couple and produce a user that is a healthy, international eater. User.template |> healthyEater |> international How about a boring local? We can even simplify a bit using type inference and drop the type name. let boringLocal = .template |> noFavoriteFoods |> domestic
— 30:12
We didn’t have to build a complicated library to get all of this functionality. We just needed our prop helper and our favorite friend: function composition!
— 30:33
All of this should remind us of how we previously styled UIKit views with functions . Everything we have done in this episode and that one is unified by just a single idea: function composition. So now we’ve seen some really great applications of this idea, and it’s only the beginning. We have a lot more we want to say on this subject. References Composable Setters Stephen Celis • Sep 30, 2017 Stephen spoke about functional setters at the Functional Swift Conference if you’re looking for more material on the topic to reinforce the ideas. https://www.youtube.com/watch?v=I23AC09YnHo Semantic editor combinators Conal Elliott • Nov 24, 2008 Conal Elliott describes the setter composition we explored in this episode from first principles, using Haskell. In Haskell, the backwards composition operator <<< is written simply as a dot . , which means that g . f is the composition of two functions where you apply f first and then g . This means if had a nested value of type ([(A, B)], C) and wanted to create a setter that transform the B part, you would simply write it as first.map.second , and that looks eerily similar to how you would field access in the OOP style! http://conal.net/blog/posts/semantic-editor-combinators Downloads Sample code 0007-setters-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 .