EP 89 · Standalone · Feb 3, 2020 ·Members

Video #89: Case Paths for Free

smart_display

Loading stream…

Video #89: Case Paths for Free

Episode: Video #89 Date: Feb 3, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep89-case-paths-for-free

Episode thumbnail

Description

Although case paths are powerful and a natural extension of key paths, they are difficult to work with right now. They require either hand-written boilerplate, or code generation. However, there’s another way to generate case paths for free, and it will make them just as ergonomic to use as key paths.

Video

Cloudflare Stream video ID: e1d1a30675baeb56f8409df8963a38e2 Local file: video_89_case-paths-for-free.mp4 *(download with --video 89)*

References

Transcript

0:05

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.

0:15

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.

0:46

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.

1:13

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.

1:28

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.

1:51

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. Reflection in Swift

2:20

Let’s start simple by implementing that example we just described: we want to implement a function such that when given any struct value, it will return an array of strings that lists every stored property name in the value. Such a function would have the following signature: func allProperties(_ value: Any) -> [String] { }

2:46

We get access to Swift’s reflection capabilities by constructing a mirror that reflects our value (get it?): func allProperties(_ value: Any) -> [String] { let mirror = Mirror(reflecting: value) }

2:57

This mirror value holds information about what is inside the value passed in. In particular, there is a children property, for which the documentation says: Note A collection of Child elements describing the structure of the reflected subject. That’s a little ambiguous, but it’s this collection that holds the information about the store properties in our struct. We can map on this collection to try to pull something useful from each child: func allProperties(_ value: Any) -> [String] { let mirror = Mirror(reflecting: value) mirror.map { child in } }

3:26

Each child has a label property and value property. The label seems promising, but it returns an optional string, so really we need a compactMap , and let’s return this value from the function: func allProperties(_ value: Any) -> [String] { return Mirror(reflecting: value).children .compactMap { $0.label } }

3:48

Then if we use it we will find that it seems to work: allProperties(users) // ["id", "isAdmin", "location", "name"]

4:01

However, we don’t know if this will work for all structs handed to this function. Swift’s reflection API is pretty bare bones, and the documentation isn’t great, so it’s not clear if this could possibly ever return more information, like say computed properties.

4:15

And that will always be the trade off of using reflection. It brings us a lot of power by being able to introspect our values at runtime, but it also comes with a lot of uncertainty. Reflecting into enums

4:26

With those caveats out of the way, let’s see what reflection reveals to us about enums. Let’s construct a value of the Authenticated type: let auth = Authentication.authenticated( AccessToken(token: "deadbeef") )

4:39

Like before we need a mirror reflecting the value to get access to its properties: let mirror = Mirror(reflecting: auth)

4:53

It turns out this collection of children only contains a single value: mirror.children.count // 1

4:57

And if we print that only child we will see something interesting: dump(mirror.children.first!) ▿ (2 elements) ▿ label: Optional("authenticated") - some: "authenticated" ▿ value: __lldb_expr_7.AccessToken - token: "deadbeef"

5:15

Interestingly the mirror has revealed to us not only the data that is held in the enum (the access token holding the string “deadbeef”), but also the name of the case this value sits in (“authenticated”). This information is held in a tuple with named components, and if we reach into the value component we get the access token: mirror.children.first!.value // AccessToken

5:33

However, this is misleading. We don’t have an honest AccessToken value, the mirror doesn’t have any of that type information: mirror.children.first!.value as AccessToken ‘Any’ is not convertible to ‘AccessToken’

5:42

The value here is of type Any , so best we can do is try to to cast it to an AccessToken : mirror.children.first!.value as? AccessToken

5:51

And already we have the beginnings of how we could implement an extraction function using reflection. We see here that just given the value alone we were able to ultimately get the access token out of the value. There were no switches, if case let s or guard case let s involved, just the reflection mirror.

6:08

Let’s try implementing a general purpose function that is capable extracting an associated value out of a root enum value: func extract<Root, Value>(from root: Root) -> Value? { }

6:30

The implementation of this is basically what we did above. We need to get a mirror of the root, try to get the first child of the mirror, pluck out the value of that child, and then try to cast it to the Value type: func extract<Root, Value>(from root: Root) -> Value? { let mirror = Mirror(reflecting: root) guard let child = mirror.children.first else { return nil } return child.value as? Value }

6:56

If we try to use this we will see there is some ambiguity in the types: extract(from: auth) Generic parameter ‘Value’ could not be inferred

7:07

This is because we haven’t given any information to extract to let it know what it is we even want to extract from the root value. We could tell it what type we expect to get things compiling: extract(from: auth) as AccessToken?

7:14

And amazingly that worked, but of course this isn’t quite right. For one thing, we’ve told it what kind of type we want to extract out of the root, but we haven’t told it what case. That’s very important information we are leaving out right now. As a silly example, what if we had an enum with two associated values that both held an integer: enum Example { case foo(Int) case bar(Int) }

7:35

If we wanted to extract an integer from a value of the Example type we’d have no way of saying which case we expect to get it from: extract(from: Example.foo(2)) as Int?

7:46

Nothing in this API allows us to say we want the integer in the bar case, not the foo case.

7:51

So how can we incorporate the case into this extract function? Well, remember that the mirror gave us access to the name of the case, it was in the label component of the child tuple: mirror.children.first!.label // "authenticated"

8:04

So, one silly thing we could do is have the extract function take the name of the case we want to extract, and then we could use that information in the implementation of the function: func extract<Root, Value>( case: String, from root: Root ) -> Value? { let mirror = Mirror(reflecting: root) guard let child = mirror.children.first else { return nil } guard case == child.label else { return nil } return child.value as? Value }

8:26

Now we have the ability to specify exactly which case we want to extract from: extract(case: "foo", from: Example.foo(2)) as Int? // 2 extract(case: "bar", from: Example.foo(2)) as Int? // nil

8:51

However a stringy API isn’t ideal because it means we could introduce a typo and the compiler wouldn’t catch it, and if we later refactor case names everything will silently break.

9:02

We do have access to something that is like a static version of this string. The case of the enum is a static symbol known by the compiler, and any typo in the name will cause an instant compiler error: Example.foo // (Int) -> Example Example.bar // (Int) -> Example

9:16

So perhaps we can use this static information instead of the string to determine which case we plan on extracting from: func extract<Root, Value>( case: (Value) -> Root, from root: Root ) -> Value? { let mirror = Mirror(reflecting: root) guard let child = mirror.children.first else { return nil } guard case == child.label else { return nil } return child.value as? Value }

9:33

Now it’s no longer valid to check the case label directly since case is a function. How can we determine if child.value is in the same case that is described by the case function passed in?

9:45

Well, we could use the case function to put the child.value back into the root, reflect on that new root, and determine if that new root is in the same case as the current child we have. Sounds like a lot, but it’s quite simple to do: func extract<Root, Value>( case: @escaping (Value) -> Root, from root: Root ) -> Value? { let mirror = Mirror(reflecting: root) guard let child = mirror.children.first else { return nil } guard let value = child.value as? Value else { return nil } let newRoot = case(value) let newMirror = Mirror(reflecting: newRoot) guard let newChild = newMirror.children.first else { return nil } guard newChild.label == child.label else { return nil } return value } Reflecting case paths

10:55

And just like that we have an API that allows us to statically determine which case we are trying to extract from an enum: extract(case: Authentication.authenticated, from: auth) // AccessToken extract(case: Authentication.authenticated, from: .unauthenticated) // nil extract(case: Result<Int, Error>.success, from: .success(42)) // 42 struct MyError: Error {} extract(case: Result<Int, Error>.failure, from: .failure(MyError())) // MyError extract(case: Example.foo, from: .foo(2)) // 2 extract(case: Example.bar, from: .foo(2)) // nil

12:30

And it seems to work! We can now generically extract out the associated value for a particular case of an enum value. And since we can do that, it means we can also magically create case paths for free by just knowing what the embed function is. We can even make a special initializer on CasePath that works given only an embed function: extension CasePath { init(_ case: @escaping (Value) -> Root) { self.embed = case self.extract = { root in extractHelp(case: case, from: root) } } }

13:29

In order to prevent confusion with the instance variable extract we also have to rename our extract function, so we chose extractHelp : func extractHelp<Root, Value>( case: @escaping (Value) -> Root, from root: Root ) -> Value? {

13:46

Now we can create case paths on the fly very easily: CasePath(Example.foo) CasePath(Example.bar) CasePath(Authentication.authenticated)

14:14

This is looking really great, not only can we delete a lot of repetitive code from earlier in the episode, but whenever we create a new enum or encounter one from a 3rd party, we don’t have to do anything to get case paths for each of its cases. We can immediately derive them with no work at all. Reflection gotchas

14:28

However, there is a huge caveat to this. As we mentioned before, anytime you are dealing with reflection you are treading on thin ice. First, the documentation of Swift’s reflection is quite sparse, and second by using reflection we are traveling outside the watchful eye of the compiler and so we have no static guarantees that we wrote our extract helper correctly.

14:48

And in fact, it isn’t quite correct right now. There are a few edge cases that are not currently supported with the current implementation. For one thing, if we use argument labels in our cases the extract helper will always fail: enum ExampleWithArgumentLabels { case foo(value: Int) } extractHelp( case: ExampleWithArgumentLabels.foo, from: .foo(value: 2) ) // nil

15:13

This fails even though it should definitely succeed. It’s possible, and in fact quite easy, to fix this, but we’ll leave that as an exercise to the viewer.

15:29

There’s another edge case, which is enums without associated values. Our extract helper doesn’t even compile for this situation because an enum case with no associated value isn’t even a function: extractHelp( case: Authentication.unauthenticated, from: .unauthenticated ) Cannot convert value of type ‘Authentication’ to expected argument type ‘(_) -> _’

15:48

So this is another edge case to solve, and it’s totally possible.

16:05

And another caveat to be aware of is that this reflection magic is only intended to work if you hand CasePath an actual enum case embed function. It’s not meant to work magically for any other situation. As a silly example, let’s say we try to construct a case path to a location’s country. let countryCasePath = CasePath<Location, String>( { country in Location(city: "Brooklyn", country: country) } )

16:53

This is of course silly because Location is a struct, and so it’s more appropriate to use key paths to focus on its parts rather than case paths. But still, nothing is preventing us from writing this strange code. And if we do that, the case path will not do what we expect: countryCasePath.extract( from: Location(city: "Brooklyn", country: "USA") ) // "Brooklyn" Here we are trying to use the country case path to extract the country out of a location, yet it returns “Brooklyn”. This is of course non-sensical, but we aren’t meant to be creating case paths for these situations anyway.

17:24

But all of that aside, we have greatly improved the ergonomics of creating case paths. We can simply wrap an enum case constructor in the CasePath initializer and we immediately get a case path. For example, the Result and Optional types are enums, and we instantly get case paths for them, not only on their cases but also for any generic used: CasePath(Result<Int, Error>.success) CasePath(Result<String, Error>.success) CasePath(Result<String, Error>.failure) CasePath(Result<String, NSError>.failure) CasePath(Optional<Int>.some) CasePath(Optional<String>.some) CasePath(Optional<[Int]>.some)

18:02

There are tons of enums out there, not only in our own code, but also in 3rd party code, like Apple’s frameworks. Foundation comes with one called DispatchTimeInterval that describes time intervals in terms of seconds, milliseconds, microseconds and nanoseconds, and we get case paths for all of them for free: CasePath(DispatchTimeInterval.seconds) CasePath(DispatchTimeInterval.milliseconds) CasePath(DispatchTimeInterval.microseconds) CasePath(DispatchTimeInterval.nanoseconds)

18:20

Even the Combine framework has a few enums of its own, like the Completion enum which describes the manner in which a publisher completes. We get case paths for it for free: CasePath(Subscribers.Completion<Error>.failure) Introducing the / operator

18:48

This is really awesome, but we can make the ergonomics even better. Let’s put a key path and a case path side-by-side so that we can see the difference in syntax:

19:08

Let’s put a key path and a case path side-by-side so that we can see the difference in syntax: \User.id CasePath(DispatchTimeInterval.seconds)

19:13

That doesn’t look super symmetrical. It would be nice if we could somehow make the syntax for a case path look a little more similar to key paths. What if it were possible to introduce a prefix operator that we can put before the enum so that we could get rid of the CasePath noise? Not only can we do that, but we can even use a forward slash as the operator, which makes it look like this: \User.id /DispatchTimeInterval.seconds

20:01

That would be pretty awesome. To accomplish this, all we need to do is declare a new operator: prefix operator /

20:24

And then define a prefix function with it that simply creates a CasePath under the hood: prefix func / <Root, Value>( case: @escaping (Value) -> Root ) -> CasePath<Root, Value> { return CasePath(case) }

20:58

And now it is totally legitimate to do this: \User.id /DispatchTimeInterval.seconds

21:11

It’s even kind of nice that key paths use a slash going one direction and case paths use a slash going the other. After all, they are different, but equal, operations. Two sides of the same coin. Duals, if you will.

21:24

If we use this prefix operator with the composition operator we can do even more powerful things, like construct a case path that traverses into the success of an enum, and then the seconds of a dispatch interval: /Result<DispatchTimeInterval, Error>.success .. /DispatchTimeInterval.seconds

22:02

But we’ve introduced yet another operator, so we should ask ourselves if it satisfies the requirements that we like an operator to satisfy.

22:22

Are we overloading an existing operator with new meaning? Well / is an infix operator that represents division in Swift, but it does not exist as a prefix operator, so we’re unlikely to cause the confusion an additional infix use of / would cause.

22:44

Does prefix / have prior art? Not really, but if we consider that it’s the mirror image of \ , which Swift uses to identify key paths, the shape seems pretty perfect.

23:12

Does it solve a universal problem or is the problem it solves more domain-specific? Well, it allows us to easily, with a single character, pluck case paths out of thin air. Swift provides this functionality for struct key paths at the language level, so a similar solution for enum case paths seems justifiable.

23:27

So here is another operator that not-quite-fully ticks all of the boxes. While we like how it bridges the ergonomic gap between key paths and case paths, we know that not everyone will be comfortable introducing an operator to their code bases. Luckily, again, the operation can still be introduced as an initializer on CasePath , and so you can still benefit from the powerful functionality with just a little extra noise. What’s the point?

23:44

So this is starting to seem pretty cool, but it’s time to take a break and ask “what’s the point?”. We have introduced a concept that is not native to Swift, and we even introduced 3 (!) operators in order to improve the ergonomics of the concept. I think we should be really sure that it is worth doing this if we are going to continue any further.

24:14

We absolutely think it’s worth exploring this topic, and it’s all due to the insight we have into the fundamental connection between structs and enums. When Swift first came out, enums were probably the strangest part of the language for a lot of us. Almost every other language out there gives us lots of tools for “product” types, like structs, classes and tuples. We developed in those languages for years, and got a lot of really great work done, without even realizing that there’s a whole other side to data types that are called “sum” types.

24:59

And this is what Swift gave us, the wonderful concept of “sum” types. The ability to describe a type that is one choice of many different types, and the ability to exhaustively deal with such values.

25:09

But, because there is such a long history of understanding and using product types in languages, and only relatively recently are sum types starting to make their way into mainstream languages, sum types are lagging behind product types in its list of features. A natural thing one wants to do with product types, and in particular structs, is to focus in on a subset of fields in the struct so that you can isolate that domain away from the whole, understand it, transform it, and then plug it back into the whole. Swift has even leap-frogged a lot of modern and advanced languages in providing 1st class support for this, which are key paths. Languages like Kotlin, Java, Haskell and Rust do not have anything like this.

25:57

And so we think that case paths are ripe for unlocking new ideas in the way we construct programs and APIs. It is the direct analog of key paths for sum types, two extremely powerful things that Swift has given us. Because this concept is so new to us it is going to take time to see its full potential. Just think back to how it was to write your first enum or make use of your first key path, and how it took time to get comfortable with using those tools.

26:46

Let’s take a quick moment to have some fun with case paths so we can begin our journey towards getting comfortable with case paths.

27:00

Right now we have quite a few enums that we have been playing round with. Let’s define a few more to make things interesting. Suppose we had a generic enum that represents the loading state of some data: enum LoadState<A> { case loading case offline case loaded(Result<A, Error>) }

27:36

With case paths we can easily express the idea of wanting to focus in on the success case of the result in the loaded case of this enum: /LoadState<Int>.loaded .. /Result.success // CasePath<LoadState<Int>, Int>

28:03

So, if we had an array of the load states, we could very easily pluck out all the successful values by using this case path. Consider the following array of states: let states1: [LoadState<Int>] = [ .loaded(.success(2)), .loaded(.failure(NSError(domain: "", code: 1, userInfo: [:]))), .loaded(.success(3)), .loading, .loaded(.success(4)), .offline, ]

28:10

It contains some loading values, some loaded values, which in turn has some successful and some failed values. If we wanted to easily extract out the integer in each success case we can simply do: states1 .compactMap(^(/LoadState.loaded .. /Result.success)) // [2, 3, 4]

29:01

That’s amazing, but let’s go even further. Say we had an array of load states of authentication values: let states2: [LoadState<Authentication>] = [ .loading, .loaded(.success(.authenticated(AccessToken(token: "deadbeef")))), .loaded(.failure(NSError(domain: "", code: 1, userInfo: [:]))), .loaded(.success(.authenticated(AccessToken(token: "cafed00d")))), .loaded(.success(.unauthenticated)), .offline ] Now we have some loading values, and some loaded values. The loaded values can be in a success or a failed state. And even the success can be in an authenticated or an unauthenticated state.

29:34

How can we pluck out all of the access tokens from this complex structure? We can construct a case path that traverses all the way down the access token by appending three case paths together: states2.compactMap( ^( /LoadState.loaded .. /Result.success .. /Authentication.authenticated ) )

30:08

However, we have one small problem: Adjacent operators are in non-associative precedence group ‘DefaultPrecedence’

30:14

We need to tell our operator how it should implicitly put parentheses around the parts of this expression. We can choose to put parentheses weighted to the the left: (/LoadState.loaded .. /Result.success) .. /Authentication.authenticated

30:36

Or to the right: /LoadState.loaded .. (/Result.success .. /Authentication.authenticated)

30:44

It actually doesn’t matter in this situation because composition of case paths is both left and right associative. It doesn’t matter how you parenthesize the expression, you will always get the same thing. So, we will just choose right associativity just to have something: precedencegroup Composition { associativity: right } infix operator ..: Composition

31:34

We can now compose these key paths without any parentheses: states2.compactMap( ^( /LoadState.loaded .. /Result.success .. /Authentication.authenticated ) ) // [{token "deadbeef"}, {token "cafed00d"}]

32:13

This is so cool. This is doing a very complicated traversal into a very complex structure.

32:19

The alternative to this would be to do explicit pattern matching in a closure: states2.compactMap { if case let .loaded(.success(.authenticated(token))) = $0 { return token } return nil }

32:52

But even this doesn’t work because multiline closures need explicit types in Swift. So we need to introduce a name for the variable in this closure, and specify the return type: states2.compactMap { state -> AccessToken? in if case let .loaded(.success(.authenticated(token))) = state { return token } return nil }

33:10

This is quite a bit more to grok at once, and there are more places where little mistakes could hide.

33:24

So we really are capable of cleaning up some code using this tool, and it’s only the beginning. We hope everyone sees how interesting the concept of case paths can be, and who knows, maybe someday Swift will give us 1st class support for the ability to analyze enums like we are able to do with structs.

33:52

We are going to uncover some great applications of case paths in many future Point-Free episodes. Anytime we want to generically work with sum types and enums, case paths are going to be our friend in allowing us to work on parts of the enum in isolation.

34:20

In fact, as soon as next week we will show to use case paths to greatly clean up some code in our composable architecture. If you are caught up with our series of architecture episodes you will know that we turned to code generation to help us operate on enums so that we could transform reducers that work with local actions into reducers that work on global actions. Well, we are going to remove all of that in favor of case paths, and it’s gonna be great. Until next time! References Extract Payload for enum cases having associated value Giuseppe Lanza • Aug 4, 2019 This Swift forum post offers a reflection-based solution for extracting an enum’s associated value and inspired our solution for deriving case paths from enum case embed functions. https://forums.swift.org/t/extract-payload-for-enum-cases-having-associated-value/27606 EnumKit Giuseppe Lanza A protocol-oriented library for extracting an enum’s associated value. https://github.com/gringoireDM/EnumKit Reflectable enums in Swift 3 Josh Smith • Apr 8, 2017 An early exploration of how an enum’s associated values can be extracted using reflection and the case name. https://ijoshsmith.com/2017/04/08/reflectable-enums-in-swift-3/ 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 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 0089-the-case-for-case-paths-pt3 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 .