EP 88 · The Case for Case Paths · Jan 27, 2020 ·Members

Video #88: The Case for Case Paths: Properties

smart_display

Loading stream…

Video #88: The Case for Case Paths: Properties

Episode: Video #88 Date: Jan 27, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep88-the-case-for-case-paths-properties

Episode thumbnail

Description

We’ve now seen that it’s possible to define “case paths”: the enum equivalent of key paths. So what are their features? Let’s explore a few properties of key paths to see if there are corresponding concepts on case paths.

Video

Cloudflare Stream video ID: 0721ed8cddb2f5bcc5d8c4454a68506a Local file: video_88_the-case-for-case-paths-properties.mp4 *(download with --video 88)*

References

Transcript

0:05

So creating a case path is quite easy, but 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.

0:30

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. There are some operations that the Swift standard library provides for key paths, and we of course would expect that there is some version of those operations for case paths too. Appending paths

0:57

To begin, key paths come with a very powerful feature that allow you to append them together in order to dive deeper into a structure. To see this let’s beef up our User struct by making it also hold a location: struct User { var id: Int var isAdmin: Bool var location: Location var name: String } struct Location { var city: String var country: String }

1:30

Then we can take the key path that goes from a user into their location: \User.location

1:37

And a key path from a location to its city: \Location.city

1:43

And we can append them together: (\User.location).appending(path: \Location.city)

1:55

With this we get a whole new key path that goes all the way from a User value to a string, which represent’s the user’s location’s city:

2:04

And Swift even gives some syntactic sugar to make this a little nicer: \User.location.city

2:30

This operation is akin to composition for functions. It says that if we have a key path from A to B and a key path from B to C , then we can create a whole new key path from A to C .

2:41

So, the question is, do case paths also support such a composition operator? They do! If we have a case path from A to B and a case path from B to C , we can create a whole new case path from A to C . To implement this we will extend the CasePath struct to add a new method that looks like the key path method: extension CasePath/*<Root, Value>*/ { func appending<AppendedValue>( path: CasePath<Value, AppendedValue> ) -> CasePath<Root, AppendedValue> { } }

4:02

So, how can we implement this? Well we know we need to return a new case path, so we might as well get a stub in place: extension CasePath/*<Root, Value>*/ { func appending<AppendedValue>( path: CasePath<Value, AppendedValue> ) -> CasePath<Root, AppendedValue> { CasePath<Root, AppendedValue>( extract: <#(Root) -> AppendedValue?#>, embed: <#(AppendedValue) -> Root#> ) } }

4:14

For the extract method we have a root from which we want to try to extract an AppendedValue . We have access to self.extract which allows us to extract a value from the root, so we can start there: extension CasePath/*<Root, Value>*/ { func appending<AppendedValue>( path: CasePath<Value, AppendedValue> ) -> CasePath<Root, AppendedValue> { CasePath<Root, AppendedValue>( extract: { root in let value = self.extract(root) }, embed: <#(AppendedValue) -> Root#> ) } }

4:33

This returns an optional Value , and we can use the case path passed in to further extract an appended value from this. Now, the value we have is optional, and path.extract wants an honest value, so we have to do some unwrapping to get this done: extension CasePath/*<Root, Value>*/ { func appending<AppendedValue>( path: CasePath<Value, AppendedValue> ) -> CasePath<Root, AppendedValue> { CasePath<Root, AppendedValue>( extract: { root in if let value = self.extract(root) { return path.extract(value) } return nil }, embed: <#(AppendedValue) -> Root#> ) } }

4:58

But this can be simplified by using the flatMap operation that is defined on optionals: extension CasePath/*<Root, Value>*/ { func appending<AppendedValue>( path: CasePath<Value, AppendedValue> ) -> CasePath<Root, AppendedValue> { CasePath<Root, AppendedValue>( extract: { root in self.extract(root).flatMap(path.extract) }, embed: <#(AppendedValue) -> Root#> ) } }

5:19

So really just is just saying that first we try to extract a value from the root, and then we try to extract an appended value from the value.

5:35

The embed function is even easier. Here we start with an appended value, which we can embed with the case path given to us. Now we have a value, which we can embed into the root using self.embed : extension CasePath/*<Root, Value>*/ { func appending<AppendedValue>( path: CasePath<Value, AppendedValue> ) -> CasePath<Root, AppendedValue> { CasePath<Root, AppendedValue>( extract: { root in self.extract(root).flatMap(path.extract) }, embed: { appendedValue in self.embed(path.embed(appendedValue)) } ) } }

5:52

And just like that we have a way to append case paths together, provided that their types match up.

5:59

To show an example use case of this, suppose we had an enum that represented the authentication state of a user. They can either be authenticated with an access token, or they can be unauthenticated: enum Authentication { case authenticated(AccessToken) case unauthenticated } struct AccessToken { var token: String }

6:21

Let’s also quickly create a case path for the authenticated case of the enum: let authenticatedCasePath = CasePath<Authentication, AccessToken>( extract: { if case let .authenticated(accessToken) = $0 { return accessToken } return nil }, embed: Authentication.authenticated )

6:47

Again we are seeing some annoying boilerplate here, but we will address this soon enough.

6:52

We can take a result case path and the authenticated case path and append them together to get a case path that traverses through the success case of a result, and then through the authenticated case to finally get access to the token: Result<Authentication, Error>.successCasePath .appending(path: authenticatedCasePath) // CasePath<Result<Authentication, Error>, AccessToken>

7:26

And now we have a case path from a Result down to an access token. Introducing the .. operator

7:46

However, using appending(path:) is quite verbose for something so simple. For key paths we could just use simple dot syntax to append key paths together, but that is done with special compiler support, so we can’t do that with key paths.

8:05

We can however introduce an infix operator that imitates the dot syntax style. It’s been awhile since we’ve used operators on Point-Free, but we can begin by first declaring the symbols we want to use for the operator: infix operator ..

8:26

And then we can implement a function with those symbols as the name of the function: func .. <A, B, C>( lhs: CasePath<A, B>, rhs: CasePath<B, C> ) -> CasePath<A, C> { return lhs.appending(path: rhs) }

9:13

Now with this operator we can express the idea of traversing into the success case of the result, and then the authenticated case of the Authentication enum via the following: Result<Authentication, Error>.successCasePath .. authenticatedCasePath

9:33

That’s quite a bit simpler and less noisy, and soon we will make this even simpler.

9:40

While some of our viewers are comfortable introducing operators to their code bases, it’s still a relatively uncommon thing to do in Swift. So in order to motivate the introduction of any particular operator, we came up with a check list of three requirements, way back in our very first episode :

10:02

First, we don’t want to overload existing operators with new meaning. This is one of the biggest sources of confusion when it comes to operators and it’s why people typically think they’re scary, because in some languages, like C++, you are only allowed to overload existing operators rather than specify new ones.

10:24

Second, we want to leverage prior art when introducing operators to Swift. Whether we’re inspired by another language or discipline, ideally we don’t invent a symbol from scratch.

10:36

Third, we believe that an operator should solve a universal problem rather than something domain-specific. Learning the meaning of an operator is a cost, so if we expect others to learn an operator’s meaning it should be applicable to a broad range of problems.

10:51

So, does this double-dot operator tick the boxes?

10:52

Well, .. does not exist in Swift, so we don’t run the risk of overloading it with new meaning. That’s good.

10:59

The .. operator has a nice shape because it arguably evokes dot-syntax, but it doesn’t have widely-recognized prior art in itself, so it perhaps only half-ticks that box.

11:12

Does .. solve a universal problem? We think it does! Swift provides such functionality to compose struct key paths at the language-level, after all, so the functionality to compose enum case paths are arguably just as important.

11:29

So, all tallied .. ticks about two to two-and-a-half boxes. We think it’s a take-it-or-leave-it operator. It can definitely clear away a lot of the fog of composing deeply into many nested enum cases, but the alternative noise of appending(path:) may be just fine for you or your team. Identity paths

11:43

There’s another nice little feature that key paths have in Swift, and it’s known as the “identity” key path.

11:58

Every single data type in Swift comes with a special key path that you access in the following way: \User.self as WritableKeyPath<User, User> \Location.self as WritableKeyPath<Location, Location> \String.self as WritableKeyPath<String, String> \Int.self as WritableKeyPath<Int, Int>

12:25

These are perfectly valid key paths, though perhaps a little silly. The “get” for these key paths simply returns the whole value, and the “set” simply replaces the whole value with a new value. Sometimes this key path can be useful when an API forces you to provide a key path, but you don’t want to actually focus on a part of your struct, you instead want to use the whole thing.

13:05

So, what does this look like for case paths? Well, case paths also have an identity, and we can easily implement it as a static variable on the CasePath type, as long as the Root and Value generics are the same: extension CasePath where Root == Value { static var self: CasePath { CasePath( embed: { $0 }, extract: { .some($0) } ) } }

14:13

So now we have an easy way to get the identity case path for any type: CasePath<Authentication, Authentication>.self Re-introducing the ^ operator

14:31

There is one more operation on key paths that we want to discuss, and it’s not something that ships with the Swift standard library, but rather it’s something we discussed on a previous Point-Free episode. Nearly two years ago we did an episode dedicated to getters and key paths , and in that episode we introduced an operator that could automatically convert a key path into a simple getter function: prefix operator ^ prefix func ^ <Root, Value>( _ kp: KeyPath<Root, Value> ) -> (Root) -> Value { return { root in root[keyPath: kp] } }

15:12

This allows us to write expressive code with very little ceremony. For example, if we had a list of users: let users = [ User( id: 1, isAdmin: true, location: Location(city: "Brooklyn", country: "USA"), name: "Blob" ), User( id: 2, isAdmin: false, location: Location(city: "Los Angeles", country: "USA"), name: "Blob Jr." ), User( id: 3, isAdmin: true, location: Location(city: "Copenhagen", country: "DK"), name: "Blob Sr." ), ]

15:21

We could easily transform this array in various ways: users.map(^\.name) // ["Blob", "Blob Jr.", "Blob Sr."] users.map(^\.location.city) // ["Brooklyn", "Los Angeles", "Copenhagen"] users.filter(^\.isAdmin).count // 2

16:01

Very soon we will not even need this prefix operator. Thanks to our very own Stephen Celis with collaboration from Greg Titus, a Swift proposal was made to allow key paths to be automatically converted to functions. It was accepted quite some time ago, and once Swift 5.2 is released we will be able to simply do the following: users.map(\.name) users.map(\.location.city) users.filter(\.isAdmin)

16:31

No special operator necessary.

16:45

Even though this operator will no longer be necessary, what it was doing was an important thing to have access to.

17:00

What should this operator do for case paths? Since on key paths the operator acted as a way to get the getter function out of the key path, it makes sense that it could be used on case paths to get access to the extract functionality. So let’s define it: prefix func ^ <Root, Value>( path: CasePath<Root, Value> ) -> (Root) -> Value? { return path.extract }

18:06

It’s quite simple, but it allows us to instantly turn any case path into an extract function: ^authenticatedCasePath // (Authentication) -> AccessToken?

18:29

We can use this much in the same way we did for key paths, except if we have an array of enum values we can try plucking out a value from the enums: let authentications: [Authentication] = [ .authenticated(AccessToken(token: "deadbeef")), .unauthenticated, .authenticated(AccessToken(token: "cafed00d")) ] authentications .compactMap(^authenticatedCasePath) // [{token "deadbeef"}, {token "cafed00d"}]

26:06

This is a very succinct way of accomplishing this. If we tried to do this without case paths we would have to destructure the authentication values ourselves: authentications .compactMap { if case let .authenticated(accessToken) = $0 { return accessToken } return nil }

19:58

But this doesn’t work because multiline closures need explicit types: authentications .compactMap { authentication -> AccessToken? in if case let .authenticated(accessToken) = authentication { return accessToken } return nil }

20:25

This is about the shortest we can get this code to be and it’s still noisy. It requires us to specify the return type of the closure since it’s multiline, and the if case let is shorter than doing a full switch , but I always have a hard time remembering the exact syntax.

20:56

But does this operator meet our requirements?

21:04

Well, ^ doesn’t exist as a prefix operator in Swift, so the only meaning we’re potentially trampling over is the prefix operator we defined way back in our episode on getters and key paths . But as we said, this operator is to case paths what it was then for key paths, so it only reinforces the original meaning.

21:46

Does this operator have prior art? Well, if you include the prior art we used it for, sure!

22:03

Does this operator solve a universal problem and not merely a domain-specific one? Definitely! Extracting a value from an enum is super common, and this operator makes it much more succinct to do so. Next time: case paths for free

22:20

Now, it might not be totally fair to show these two snippets side-by-side and say how clean one is compared to the other: authentications .compactMap(^authenticatedCasePath) authentications .compactMap { authentication -> AccessToken? in if case let .authenticated(accessToken) = authentication { return accessToken } return nil }

22:28

The first snippet has the exact same boilerplate, it’s just been hidden away inside the case path, and so we don’t see it at the call site. That might make it a little nicer, but the boilerplate is still there.

22:40

However, something can even be done about that boilerplate. As we have seen, the boilerplate in creating case paths has to do with the extract function, which tries to extract an associated value from an enum. The embed function comes for free in Swift because each case of an enum acts as a function that can embed the associated value into the enum, but the extract takes some work.

22:50

One way to try to get rid of this boilerplate is to turn to code generation. In fact, this is what we did in our episode on enum properties , where we showed how we could give enums an API to access the data it held that had the same ergonomics as simple dot syntax that structs can use. The API came with some boilerplate, and so we then a tool over a few episodes ( part 1 , part 2 , part 3 ) that uses Swift Syntax to analyze our source code and automatically generate the enum properties for all of the enums in our project.

23:21

That was really powerful, and we could maybe turn to code generation for case paths, but also code generation is quite heavy. We need to find the best way to run the tool whenever source code changes to make sure it’s up to date, and that can complicate the build process.

23:48

It turns out, for case paths in particular we can do something different. We can magically derive the extract function for a case path from just the embed function that comes from the case of an enum. We say this is “magical” because it uses Swift’s runtime reflection capabilities.

24:03

If you are not familiar with the idea of reflection in programming, all you need to know is it allows you to inspect the internal structure of values and objects at runtime. For example, you can use reflection to get a list of all of the string names of stored properties that a struct has.

24:26

Any time you use reflection in Swift you are purposely going outside the purview of the Swift compiler. This means you are in dangerous waters since the compiler doesn’t have your back. However, if you tread lightly and write lots of tests, you can come up with something somewhat reasonable that can clear away all of the repetitive boilerplate. Let’s start by exploring the reflection API a bit and see what is available to us…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 Getters and Key Paths Brandon Williams & Stephen Celis • Mar 19, 2018 In this episode we first define the ^ operator to lift key paths to getter functions. Note 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. https://www.pointfree.co/episodes/ep8-getters-and-key-paths 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 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 How Do I Write If Case Let in Swift? Zoë Smith This site is a cheat sheet for if case let syntax in Swift, which can be seriously complicated. http://goshdarnifcaseletsyntax.com 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 0088-the-case-for-case-paths-pt2 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 .