EP 87 · The Case for Case Paths · Jan 20, 2020 ·Members

Video #87: The Case for Case Paths: Introduction

smart_display

Loading stream…

Video #87: The Case for Case Paths: Introduction

Episode: Video #87 Date: Jan 20, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep87-the-case-for-case-paths-introduction

Episode thumbnail

Description

You’ve heard of key paths, but…case paths!? Today we introduce the concept of “case paths,” a tool that helps you generically pick apart an enum just like key paths allow you to do for structs. It’s the tool you never knew you needed.

Video

Cloudflare Stream video ID: 6e1acf8a7e3678a6ece9170557b7c260 Local file: video_87_the-case-for-case-paths-introduction.mp4 *(download with --video 87)*

References

Transcript

0:05

We have explored the idea that Swift’s structs and enums are intimately and fundamentally related to each other in many past Point-Free episodes. We first did this in our episodes on algebraic data types , where we showed that in a very precise sense enums correspond to addition that we all know and love from algebra, and structs correspond to multiplication. We even showed that certain algebraic properties also carry over to Swift, like the fact that multiplication distributes over addition means that you can refactor your own data types to factor out common pieces of data.

0:43

We took this idea even further in another episode where we claimed that literally any feature you endow structs with there should be a corresponding version of it for enums, and vice versa. There are a few examples of where this principle holds, or mostly holds, for various features of structs and enums. However, in general this is not the case, and there are many instances where Swift clearly favors structs over enums where Swift gives structs many features that enums do not get.

1:08

One particular example we looked at was how ergonomic it is to access the data inside a struct. One can simply use dot syntax to get and set any field inside a struct value. The same is not true of enums, you have no choice but to do a switch , if case let or guard case let , which are syntactically heavy when all you wanna do is access a little bit of data inside an enum.

1:27

This led us down the path of defining what we called “ enum properties ”, which give enums the power of accessing their data with simple dot-syntax just like structs. We even created a code generation tool ( part 1 , part 2 , part 3 ), using Apple’s Swift Syntax library, to automatically generate these properties for us to make it as easy as possible.

1:44

We want to take that work even further and discuss another feature that Swift structs are blessed with but which sadly has no representation for enums, and that is key paths. Key paths are a powerful feature in Swift, and we’ve talked about and used them a ton on Point-Free. Every field on a struct magically gets a key path generated for it by the compiler. But what does the corresponding concept look like when applied to enums? Answering this question will give us a tool for unlocking even more power from enums, and will even help us greatly simplify some code we previously wrote for the composable architecture .

2:21

Let’s start by reminding ourselves what key paths are and why they are so powerful. Key paths: a refresher

2:35

Let’s start by defining a simple struct to play around with: struct User { var id: Int var isAdmin: Bool var name: String }

2:38

Each of these fields has a corresponding key path that is accessible by using a special backslash syntax: \User.id

2:51

Unfortunately due to a bug in Swift playgrounds this prints out some really weird type information, but the type of this value is: \User.id as WritableKeyPath<User, Int>

3:04

And all of these fields have a key path: \User.id as WritableKeyPath<User, Int> \User.isAdmin as WritableKeyPath<User, Bool> \User.name as WritableKeyPath<User, String>

3:12

At its core, all a writable key path is is a pair of getter and setter functions. You get access to its “get” functionality by using the following special subscripting syntax: var user = User(id: 42, isAdmin: true, name: "Blob") user[keyPath: \User.id] // 42

3:39

You can even use type inference to omit the User from this since the compiler can figure it out: user[keyPath: \.id]

3:48

Further you can access the “setter” functionality of the key path by doing the following: user[keyPath: \.id] = 57

4:05

We get access to this setter functionality because the fields of the User struct are all var s. If we had instead used let s in the struct, we would only get a KeyPath instead of a WritableKeyPath . For example, let’s flip the isAdmin field to be a let : struct User { var id: Int let isAdmin: Bool var name: String }

4:22

And we can check to see if isAdmin is a WritableKeyPath : \User.isAdmin as WritableKeyPath<User, Bool> ‘KeyPath<User, Bool>’ is not convertible to ‘WritableKeyPath<User, Bool>’

4:33

And we see that it is not. It is, however, a plain old KeyPath : \User.isAdmin as KeyPath<User, Bool>

4:38

A KeyPath like this has only the getter functionality because you can’t possibly set the isAdmin field, after all it’s a let !

4:43

Now of course you would never use a key path like this because it is far nicer to just use dot-syntax directly: user.id = 57 user.name = "Blob Jr." Key paths in practice: bindings

5:02

Where key paths really shine is when they are used in generic functions because they allow you to pass along this getter/setter functionality essentially for free. There are lots of applications of key paths, but one common use for them is to facilitate the idea of setting up a binding relationship between two objects.

5:20

The idea is that you would have some kind of object that represents a piece of UI on the screen, like say a label: class Label { var font = "Helvetica" var fontSize = 12 var text = "" }

5:29

Of course if you were using UIKit you would use a UILabel instead, but this idea works for things even outside of UIKit world.

5:35

Next, you would have some kind of model object that represents the state of your application: class Model { var userName = "" }

5:44

And we would want to “bind” this model’s userName field to the text field of the label so that any change to this model would be immediately reflected in the UI. The API to do such a thing might look like this: let model = Model() let label = Label() bind(model: model, \.userName, to: label, \.text)

6:12

Then as the model changes over time the label would automatically update.

6:22

If we are willing to use the Objective-C runtime we can actually implement bind quite easily, and in fact we have an implementation in the Sources directory of this playground. The implementation is not important at all so we will not discuss it, but we can make use of it as long as we make our Model and Label classes visible to the Objective-C runtime: class Label: NSObject { @objc dynamic var font = "Helvetica" @objc dynamic var fontSize = 12 @objc dynamic var text = "" } class Model: NSObject { @objc dynamic var userName = "" }

6:52

With these changes we can now see that any mutation we make to model is instantly reflected in label : label.text model.userName = "blob" label.text // "blob" model.userName = "XxFP_FANxX93" label.text // "XxFP_FANxX93"

7:37

It’s worth noting that key paths make the call site of this API read very nicely and fluidly: bind(model: model, \.userName, to: label, \.text)

7:42

We can read it as literally “bind model’s username to label’s text”.

7:54

On the other hand, if we did not have key paths, the API would have to require that we pass in explicit getters and setters for these fields. This might look something like this: bind( model: model, get: { $0.userName }, to: label, get: { $0.text }, set: { $0.text = $1 } )

8:28

The userName field only needs a getter, but the label’s text needs both a getter and a setter. This has not only made the call site of the API look gnarlier, but we had to write extra code to accomplish this. This is what we mean when we previously said that key paths are like “compiler generated code”, it can replace all this boilerplate of creating closures to access and set fields in a value. Key paths allow us to bundle all of this work up into a single package, and the compiler creates those packages for us automatically.

8:53

This kind of UI binding is very common to do in UIKit, but SwiftUI tends to take care of a lot of these kinds of things for us without us really thinking about it. However, Apple’s Combine framework provides this kind of binding API for its publishers, and it looks quite similar to what we just did above: let subject = PassthroughSubject<String, Never>() subject.assign(to: \.text, on: label) subject.send("MaTh_FaN96") label.text // "MaTh_FaN96" Key paths in practice: reducers

10:40

There’s another application of key paths we want to demonstrate really quickly, and it’s something we saw on Point-Free when we discussed the composable architecture . We want to stress that it is not necessary to understand all of our past episodes on the Composable Architecture to understand this example. But, we feel it would be good to see another place that key paths are incredibly useful and help clean up our code.

11:01

We came across key paths when we were looking for ways to transform reducers, which are the core unit that power our architecture. At its essence a reducer is simply a function with the following signature: typealias Reducer<Value, Action> = (inout Value, Action) -> Void

11:14

It represents a process that can mutate a value given an action, typically a user action.

11:20

The reducers we dealt with in the Composable Architecture were a little more complicated than this, but this signature will serve the purpose we have right now.

11:28

We call this a reducer because it’s precisely what you feed into the reduce(into:) function that is in the standard library on arrays: [1, 2, 3].reduce(into: <#Result#>, <#(inout Result, Int) throws -> ()#>) [1, 2, 3].reduce(into: 0) { $0 += $1 } [1, 2, 3].reduce(into: 0, +=)

12:09

And while exploring the Composable Architecture we found that we would often have reducers that work on small subsets of data of our entire application, but we wanted somehow to transform them so that they would work with the entirety of application data. We wanted this because it would allow us to take lots of disparate little reducers that just concentrate on the data they care about, and then plug them all together into one big reducer.

12:35

To do this, we ultimately came to the conclusion that we needed to implement a function with the following signature: func pullback<GlobalValue, LocalValue, Action>( reducer: @escaping Reducer<LocalValue, Action>, value: WritableKeyPath<GlobalValue, LocalValue> ) -> Reducer<GlobalValue, Action> { fatalError() }

12:43

This allows us to “pull back” reducers that work on local values to one that works on global values. You give it a reducer that operates on local values, and a key path that can transform global values into local values, and it will give you back a reducer that can work on global values.

13:04

The implementation of this function is quite simple, in fact it almost writes itself. We can start by returning a function with the reducer’s signature since we know we have to do at least that: func pullback<GlobalValue, LocalValue, Action>( reducer: @escaping Reducer<LocalValue, Action>, value: WritableKeyPath<GlobalValue, LocalValue> ) -> Reducer<GlobalValue, Action> { return { globalValue, action in } }

13:23

Then inside here we have access to a global value and a reducer that works on local values. So, we can use the key path to extract the local value from the global value, run the reducer on that local value, and then plug that local value back into the global value: func pullback<GlobalValue, LocalValue, Action>( reducer: @escaping Reducer<LocalValue, Action>, value: WritableKeyPath<GlobalValue, LocalValue> ) -> Reducer<GlobalValue, Action> { return { globalValue, action in var localValue = globalValue[keyPath: value] reducer(&localValue, action) globalValue[keyPath: value] = localValue } }

14:17

Notice that we are using both the getter and setter functionality of the key path in this function. If we only had getter functionality we would not be able to implement this.

14:30

Further, because the reducer uses an inout parameter, this can all be done in a single line: func pullback<GlobalValue, LocalValue, Action>( reducer: @escaping Reducer<LocalValue, Action>, value: WritableKeyPath<GlobalValue, LocalValue> ) -> Reducer<GlobalValue, Action> { return { globalValue, action in // var localValue = globalValue[keyPath: value] // reducer(&localValue, action) // globalValue[keyPath: value] = localValue reducer(&globalValue[keyPath: value], action) } }

15:15

And thanks to this transformation we are allowed to do powerful things like take a reducer on just plain integers: let counterReducer: Reducer<Int, Void> = { count, _ in count += 1 }

15:48

And pull it back to a reducer that operates on users by just incrementing their id : pullback(reducer: counterReducer, value: \User.id)

16:27

This last line of code right is packing a serious punch because it gives us a super lightweight way to transform reducers from one domain into another. This is akin to how powerful it can be to use the map operation to transform arrays: [1, 2, 3].map(String.init) // ["1", "2", "3"]

16:52

If you remember what it was like to code before you knew about the map operation you will know how much of a pain it was to manage for loops and mutation just to do something as simple as this. This is what the pullback operation does for us, but for reducers instead. It keeps us from having to write a bunch of boilerplate code, and we can work on higher level concepts.

17:15

If we lived in a world that didn’t have key paths, the pullback function would have been forced to take in a getter and setter function in addition to the reducer: func pullback<GlobalValue, LocalValue, Action>( reducer: @escaping Reducer<LocalValue, Action>, getLocalValue: @escaping (GlobalValue) -> LocalValue, setLocalValue: @escaping (inout GlobalValue, LocalValue) -> Void ) -> Reducer<GlobalValue, Action> {

17:52

Then to implement this we need to perform multiple steps to extract the local value, run the reducer, and then set the new local value inside the global value: func pullback<GlobalValue, LocalValue, Action>( reducer: @escaping Reducer<LocalValue, Action>, getLocalValue: @escaping (GlobalValue) -> LocalValue, setLocalValue: @escaping (inout GlobalValue, LocalValue) -> Void ) -> Reducer<GlobalValue, Action> { return { globalValue, action in var newLocalValue = getLocalValue(globalValue) reducer(&newLocalValue, action) setLocalValue(&globalValue, newLocalValue) } }

18:39

And then to use this function we need to supply both a getter and a setter so that it can do its job: pullback( reducer: counterReducer, getLocalValue: { $0.id }, setLocalValue: { $0.id = $1 } )

19:03

But this doesn’t work because the compiler doesn’t know what type $0 is. We need to give it a hint like we did for the key path, which we can do like this: pullback( reducer: counterReducer, getLocalValue: { (user: User) in user.id }, setLocalValue: { $0.id = $1 } )

20:01

This is exactly the code that the Swift compiler can write for us when we use key paths. All of this boilerplate for creating closures that simply pluck out a field from a struct and stick it back in. Key paths make this trivial: pullback(reducer: counterReducer, value: \User.id)

20:15

Again, this is why we call key paths “compiler generated code.” Introducing: case paths

20:18

So now that we’ve seen that key paths are incredibly powerful in Swift, and that structs are automatically blessed with a key path for each of their fields, it’s time to ask: what is the corresponding story for enums?

20:29

As we have seen in previous episodes, and hopefully you are convinced, anytime structs are blessed with some kind of feature or functionality we should immediately begin to wonder what does the corresponding story look like for enums. Without asking this we are only seeing half the story, and potentially missing out on some really great functionality that could help clean up our code today, or even better, could help us design APIs that we are not even thinking about because we don’t have the tools and concepts today.

21:06

To understand what the key path equivalent for enums would be, let’s look at the shape that key paths have in Swift.

21:20

The actual implementation of key paths in Swift is a bit complicated, and it’s modeled as a class hierarchy for various reasons, but at its core it’s simply a pair of getter and setter functions, which we can wrap up in a struct: struct _WritableKeyPath<Root, Value> { let get: (Root) -> Value let set: (inout Root, Value) -> Void }

21:34

To prevent confusion with the standard library’s type we have prefixed the type with an underscore.

21:38

So, all a key path is is a get function, which allows us to extract a value from a root, and a set function, which allows us to mutate a root given a new value.

21:49

Let’s begin to translate this over into what we envision the equivalent API is for enums. First, what should we call it? The name “key path” most likely comes from naming used in Objective-C and so-called “key-value observation”, also known as KVO. In KVO a key path is a string that navigates to some data inside a structure, which can look like this in practice: // [user valueForKeyPath:@"location.city"]

22:23

The name “key” is probably used since we are using a string representation of a field of the structure, and “path” is probably used because you can travel through multiple keys at once. Swift’s key paths help accomplish the same thing as this, but do so in a static, type-safe way, and so that is probably why they are also called key paths.

22:43

Another appropriate name might be “property path” since each property of a data type gives rise to a key path. It’s not quite as short as “key path”, but it’s a perfectly fine name to use, and we can use it for inspiration to figure out a name for enums. Since each property on a struct leads to a key path, we would expect that each case of an enum leads to something. So perhaps a good name for this is a “case path”: struct CasePath

23:06

Like key paths, which are generic over their struct Root and property Value , case paths will be generic over their enum Root , and case Value . struct CasePath<Root, Value> { }

23:25

And just as key paths express two fundamental operations that one can do with a struct, case paths should express the fundamental operations one can do with an enum. With structs we can “get” a property out of a struct, but with enums we can try to “extract” a case from an enum. This extract does not always succeed, and therefore it needs to be failable: struct CasePath<Root, Value> { let extract: (Root) -> Value? }

23:51

This extract function is the enum version of the get function for structs. It allows us to get at the data on the inside of the enum, it just doesn’t always succeed in doing that.

24:00

The other fundamental operation that properties on structs have is the ability to mutate the whole of the struct by setting a property. With enums we can do something similar, but we instead can take an associated value of one of the cases, and embed it into the root enum: struct CasePath<Root, Value> { let extract: (Root) -> Value? let embed: (Value) -> Root }

24:22

This embed function is the enum version of the set function for structs. It strangely doesn’t need both a root and a value to do its job like set does, but this is because enums come with a very easy way to embed values into them.

24:34

This is our definition of CasePath , the analog of struct key paths, but for enums. There is a very strong connection between case paths and key paths, they truly are two sides of the same coin just as enum and structs are too.

24:47

Key paths allow us to pick apart a struct and analyze and update its parts in isolation. For example, the name key path for the User type allows us to focus in on the name field, apply transformations if we want, and then zoom back out to the entirety of the user. Case paths allow us to do the same, except for enums. We can focus on just one case of the enum, consider it in isolation, and then zoom out to the entirety of the enum. Defining a couple case paths

25:09

To get a feel for this, let’s create a few case paths from scratch. One enum that ships with the standard library is the Result type, and we can create case paths for each of its cases. Because the Result type is generic we cannot create a single CasePath that works for all possible Result parameterization: let successCasePath: CasePath<Result<Success, Failure>, Success> Use of undeclared type ‘Success’

25:43

A hacky workaround for this is to store the case path inside the Result type, that way we get free access to its generics: extension Result { static var successCasePath: CasePath<Result, Success> { CasePath<Result, Success>( extract: { result in if case let .success(success) = result { return success } return nil }, embed: Result.success ) } }

27:06

And we can do the same for the .failure case: static var failureCasePath: CasePath<Result, Failure> { CasePath<Result, Failure>( extract: { result in if case let .failure(failure) = result { return failure } return nil }, embed: Result.failure ) }

27:32

So creating a case path is quite easy. The extract just needs to check if the enum value is in the case we expect, and if it is we return its associated value, and otherwise we return nil . The embed is even easier because the case of an enum acts as a function from the associated value into the enum.

27:48

With these defined, we can access these case paths directly on Result . Result<Int, Error>.successCasePath // CasePath<Result<Int, Error>, Int> Result<Int, Error>.failureCasePath // CasePath<Result<Int, Error>, Error>

28:07

Now, there are a lot of things not completely right with this code snippet. First, it is not right to pollute the Result namespace with these static vars, it would be better to have a different place to store them. Also, there is some boilerplate involved in creating these case paths. We see it here with this if case let stuff when implementing the extract , and basically every case path we create will look exactly like this, and after awhile it’s going to be a pain to have to write over and over. Next time: the properties of case paths

28:31

We are going to solve both of those problems soon, but first we want to explore some of the properties of case paths and show the similarities with key paths…next time! References Structs 🤝 Enums Brandon Williams & Stephen Celis • Mar 25, 2019 In this episode we explore the duality of structs and enums and show that even though structs are typically endowed with features absent in enums, we can often recover these imbalances by exploring the corresponding notion. Name a more iconic duo… We’ll wait. Structs and enums go together like peanut butter and jelly, or multiplication and addition. One’s no more important than the other they’re completely complementary. This week we’ll explore how features on one may surprisingly manifest themselves on the other. https://www.pointfree.co/episodes/ep51-structs-enums Make your own code formatter in Swift Yasuhiro Inami • Jan 19, 2019 Inami uses the concept of case paths (though he calls them prisms!) to demonstrate how to traverse and focus on various parts of a Swift syntax tree in order to rewrite it. Note Code formatter is one of the most important tool to write a beautiful Swift code. If you are working with the team, ‘code consistency’ is always a problem, and your team’s guideline and code review can probably ease a little. Since Xcode doesn’t fully fix our problems, now it’s a time to make our own automatic style-rule! In this talk, we will look into how Swift language forms a formal grammar and AST, how it can be parsed, and we will see the power of SwiftSyntax and it’s structured editing that everyone can practice. https://www.youtube.com/watch?v=_F9KcXSLc_s Swift Tip: Bindings with KVO and Key Paths Chris Eidhof & Florian Kugler • Apr 24, 2018 This handy Swift tip shows you how to create bindings between object values using key paths, similar to the helper we used in this episode. https://www.objc.io/blog/2018/04/24/bindings-with-kvo-and-keypaths/ Introduction to Optics: Lenses and Prisms Giulio Canti • Dec 8, 2016 Swift’s key paths appear more generally in other languages in the form of “lenses”: a composable pair of getter/setter functions. Our case paths are correspondingly called “prisms”: a pair of functions that can attempt to extract a value, or embed it. In this article Giulio Canti introduces these concepts in JavaScript. https://medium.com/@gcanti/introduction-to-optics-lenses-and-prisms-3230e73bfcfe Optics By Example: Functional Lenses in Haskell Chris Penner Key paths and case paths are sometimes called lenses and prisms, but there are many more flavors of “optics” out there. Chris Penner explores many of them in this book. https://leanpub.com/optics-by-example Downloads Sample code 0087-the-case-for-case-paths-pt1 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 .