Video #257: Macro Case Paths: Part 1
Episode: Video #257 Date: Nov 13, 2023 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep257-macro-case-paths-part-1

Description
“Case paths” grant key path-like functionality to enum cases. They solve many problems in navigation, parsing, and architecture, but fall short of native key paths…till now. Let’s close this gap using macros that generate actual key paths to enum cases.
Video
Cloudflare Stream video ID: ed1b222414ccf315717130d9cbb8a99c Local file: video_257_macro-case-paths-part-1.mp4 *(download with --video 257)*
Transcript
— 0:05
About 2 months ago we did quite a deep dive into macros. We showed how they work as a compiler plugin, we showed how to debug them, how to write deep and nuanced tests of them, including their diagnostics and fix-its, and we even played around with SwiftSyntax a bit to improve some open source macros from Apple and the community.
— 0:23
We are now ready to start creating a new macro from scratch that will help improve one of our most popular libraries, and we are going to use all of the knowledge we built up from those past episodes. Brandon
— 0:32
There’s a tool that we built nearly 4 years ago that will greatly benefit from macros, and that’s “case paths.” Case paths are the dual concept to key paths in Swift that we all know and love. Key paths give you an abstract representation of the getter and setter for a mutable property on a type that you can reference without having an actual value at hand. You can pass this representation around in your program, and it is an important concept used in various algorithms in the Swift ecosystem, and is used a ton in SwiftUI. Honestly, key paths are one of the most powerful features in the Swift language, and there is no other programming language out there today, to our knowledge, that has first class support for such a concept. Stephen
— 1:14
So, key paths are amazing, but sadly they do not work on enums. Structs and enums are like two sides of the same coin. It doesn’t make sense to talk about one without thinking about the other, and we’ve spent many episodes in the past trying to convince everyone of this fundamental fact. But unfortunately Swift often provides a lot of tools and affordances to structs without providing the corresponding tool for enums. And key paths are one such example. Brandon
— 1:38
That is why we defined the concept of a “case path” in episodes nearly 4 years ago. They allow you to abstractly represent the two most fundamental operations you can do with a case of an enum: you can try to extract the payload from an enum value or you can embed a payload value into an enum. So key paths have getters and setters whereas case paths have extractors and embedders. And just as there are a lot of algorithms that make use of key paths, there are also a lot of algorithms that can make use of case paths. We have shown how powerful they can be in the Composable Architecture, but they are also useful in vanilla SwiftUI, and in parser-printers, and a whole bunch more places. Stephen
— 2:18
We’ve tried over the years to drum up interest for first-class support of case paths in the Swift language, but nothing has really taken hold. So, it’s time to see how far we can get with macros to make this concept seem as native as possible in the Swift language. It still won’t be as good as if Swift supported it natively, but we can get a lot closer than what was possible before macros.
— 2:41
Let’s start with a quick introduction to case paths so that we all know what we are talking about here. Key path basics
— 2:47
Even before discussing case paths, let’s remember why key paths are so cool. Here’s a very simple struct with a let field and a var field: import Foundation struct User { let id: UUID var name: String }
— 2:56
Without doing any other extra steps Swift magically produces two key paths for this struct, which can be referred to by this shorthand syntax: \User.id // KeyPath<User, UUID> \User.name // WritableKeyPath<User, String>
— 3:25
A KeyPath is an abstract representation of the idea of getting a field from the User struct, and a WritableKeyPath is an abstract representation of the idea of getting and setting a field on the User struct. The id key path is not writable because that field is a let in the struct definition.
— 3:41
You can then use the key path to access and mutate a user by using the subscript syntax: var user = User(id: UUID(), name: "Blob") user[keyPath: \User.id] // UUID user[keyPath: \User.name] = "Blob Jr."
— 4:25
Now this is of course much more verbose than using simple dot syntax: user.id user.name = "Blob Jr."
— 4:34
But key paths aren’t meant to be used in this concrete manner. They are meant to be used in generic situations where you know nothing about the underlying data type other than you have a key path focused in on some sub-part of the data. It can be incredibly powerful.
— 4:48
For one quick example of key path usage one only needs to look at how key paths are allowed to be promoted to closures in certain situations. For example, suppose you had an array of user values like so: let users = [ User(id: UUID(), name: "Blob"), User(id: UUID(), name: "Blob Jr."), User(id: UUID(), name: "Blob Sr."), ]
— 4:59
If we wanted to pluck out just the name from each of these users we could of course do so with the map operation on arrays: users.map { $0.name }
— 5:16
This works just fine, and there is absolutely nothing wrong with it, but key paths give us a slightly nicer syntax for this: users.map(\.name)
— 5:28
Because key paths represent a getter defined on some root value, Swift allows key path literals to be promoted to functions that go from a root type to a value.
— 5:39
So, that’s nice, but it’s also barely scratching the surface of what key paths are capable of. All we have done here is reduce some code by 4 characters and reduce some noise thanks to the fact that we don’t need to refer to $0 anymore. But overall, if I had to write the longer form of this expression everyday instead of the key path version, I don’t think my life would be substantially worse off.
— 5:58
Where key paths really start to show their power is when they are used in generic algorithms that make use of both the getting and setting capabilities. This is used heavily throughout SwiftUI and Apple’s frameworks. For example, in SwiftUI there is something known as a “binding” which allows you to make mutations to a piece of state that is owned by a parent view: @Binding var user = User(id: UUID(), name: "Blob") $user // Binding<User>
— 6:36
Thanks to the power of dynamic member lookup in Swift you can dot-chain onto a binding to access a property inside the User in a way that derives a whole new binding: $user.name // Binding<String>
— 6:50
This only works because the User type is naturally endowed with a writable key path for its name field, which allows deriving a new binding that uses the key path under the hood.
— 7:01
Even though SwiftUI is closed source, we can guess what this binding derivation code roughly looks like. It will be a dynamic member subscript on the Binding type that returns a whole new binding: extension Binding { subscript<Member>( dynamicMember keyPath: WritableKeyPath<Value, Member> ) -> Binding<Member> { } }
— 7:43
And we can create a brand new binding from scratch such that it’s get and set endpoints simply call out to the underlying binding with the key path: extension Binding { subscript<Member>( dynamicMember keyPath: WritableKeyPath<Value, Member> ) -> Binding<Member> { Binding<Member>( get: { self.wrappedValue[keyPath: keyPath] }, set: { self.wrappedValue[keyPath: keyPath] = $0 } ) } }
— 8:08
This is only possible thanks to the fact that Swift automatically generates these things for us, and thanks to the fact that it encapsulates the getting and setting functionality for a field. Both are necessary for this binding transformation. If we only had the getter, this would not be possible.
— 8:30
SwiftUI also uses key paths in its tools for overriding and propagating environment values throughout a view hierarchy. If you have ever used the .environment view modifier to make a change to a value, such as the color scheme: .environment(\.colorScheme, .dark)
— 8:52
…then you were using key paths. And again it is crucial that this key path have both getting and setting functionality.
— 9:03
There are even some key path APIs in the Swift standard library. For example, you can get an unsafe pointer to a mutable value, such as the user value, like so: withUnsafePointer(to: &user) { userPtr in }
— 9:33
You can further get an unsafe mutable pointer to the user like so: withUnsafeMutablePointer(to: &user) { userPtr in }
— 9:45
In the closure we can further get an unsafe pointer to the name field of the user : let namePtr = withUnsafePointer(to: &user) { userPtr in userPtr.pointer(to: \.name) } This pointer(to:) API takes a KeyPath in order to implement its logic.
— 10:03
And crucially, this requires \.name to be a WritableKeyPath since it is a mutable pointer. If we tried using a non-mutable field, like id , then we would only get a plain unsafe pointer out the other side, not a mutable pointer.
— 10:22
We also use key paths extensively in our Composable Architecture library. If we just search for “WritableKeyPath<” in the project we will find dozens of uses, most of which are related to composing reducers where it is necessary to be able to read a small bit of child state from a parent feature, perform a mutation on it, and then stick the state back into the parent state. Again, the getting and setting functionality is absolutely necessary.
— 11:06
If we didn’t have native support for key paths in Swift you would be forced to write out these getters and setters yourself. Something like this: let getUserID: (User) -> UUID = { $0.id } let getUserName: (User) -> String = { $0.name } let setUserName: (inout User, String) -> Void = { $0.name = $1 }
— 11:49
Though you would probably want to house these variables somewhere besides the module scope like this. So, for the lack of a better word, you might be tempted to define a type that bundles the getter and setter together: struct GetterAndSetter<Root, Value> { var get: (Root) -> Value var set: (inout Root, Value) -> Void }
— 12:12
And then you could house the concrete getters and setters for a type as static variables on the type: extension GetterAndSetting where Root == User, Value == String { static var name: Self { Self { $0.name } set: { $0.name = $1 } } }
— 12:33
But of course things get more complicated when you need to support things like immutable fields, such as id , which don’t have a setter. And you would want the concept of a getter/setter pair naturally degrading to just a getter if possible. But we aren’t even going to attempt to sketch out what a solution to that would look like.
— 12:47
So, long story short, key paths are incredibly handy, and it’s amazing that Swift generates them for us automatically and that we can write generic algorithms over the shape of data types using them.
— 12:57
But what is not so great is that enums are completely left out of this story. If you have an enum with a few cases: enum Loading { case inProgress case loaded(String) }
— 13:10
And you wanted to abstractly refer to the loaded case without actually having access to a Loading value: \Loading.loaded
— 13:24
…well, you’re out of luck. The key path syntax does not work for cases.
— 13:30
And even if it did work, but kind of key path would it be? To keep things simple I suppose it could just be a non-writable key path into an optional string: \Loading.loaded // KeyPath<Loading, String?>
— 13:40
It’s optional because it is not always possible extract loaded associated data from a Loading value. For example, if you have an inProgress value then we would have no choice but to return nil : var value = Loading.inProgress value[keyPath: \.loaded] // nil
— 14:01
This is all theoretical, but if we did go this route then we are missing some important functionality from key paths. Just as we saw a moment ago with all of our examples, a key path is most powerful when it is used in conjunction as a getter and a setter.
— 14:17
However, trying to stretch the definition of a setter to also cover enums leads us to some very weird places. Say for a moment that key path syntax could be extended to enums and it that it actually gave us a writable key path instead of just a plain key path: \Loading.loaded // WritableKeyPath<Loading, String?>
— 14:23
That would allow you to do something quite non-sensical, such as setting the loaded case on an inProgress value: var value = Loading.inProgress value[keyPath: \.loaded] = "Hello" // ???
— 14:36
What should this do? Should it update value to point to the loaded case, or should it be a no-op since the original value was pointed at the inProgress case?
— 14:47
Even weirder, you can also set with a nil value: value[keyPath: \.loaded] = nil // ???
— 14:52
This has no choice but to be a no-op, but it seems very weird to even have this be possible. Case path basics
— 14:57
None of this really makes sense, and that’s because we are shoehorning in the semantics of a key path into something that is actually distinct from getters and setters. Enums don’t have getters and setters, they instead have “extractors” and “embedders.” Brandon
— 15:10
And this is what led us to define case paths as a first class concept in hopes that it would allow us to bring the power and ergonomics of key paths to enums. We ultimately open sourced a library with these tools, but never felt we truly achieved the promise of case paths.
— 15:26
Let’s take a quick look at what case paths look like today.
— 15:30
The two most fundamental things you can do with an enum is try to extract a payload for a case: let extractLoaded: (Loading) -> String? = { guard case let .loaded(value) = $0 else { return nil } return value }
— 16:19
And embed a payload into a case: let embedLoaded: (String) -> Loading = { Loading.loaded($0) }
— 16:44
This last one can even be written as succinctly as this: let embedLoaded: (String) -> Loading = Loading.loaded
— 16:54
…since the case of an enum doubles as a static function from the payload value to the enum.
— 17:01
These two fundamental operations are distinct enough from key paths that they warrant their own name, and it’s what we like to call a case path. And since Swift doesn’t have a native representation of this concept, we are forced to define our own type to bundle up the operations: struct CasePath<Root, Value> { let extract: (Root) -> Value? let embed: (Value) -> Root }
— 17:35
Case paths are just as useful as key paths, they just work best on enums rather than structs.
— 17:56
We have lots of places we use them in our various libraries. For example, in our Composable Architecture library case paths are also used for reducer composition. We can search the library for “CasePath<” to see all the places it is used. It allows us to attempt to extract a child action from a parent action, and if that succeeds run the child reducer on the child state, and then whatever effect it produces can be transformed into a parent effect using the embed functionality. Both the extract and embed functionality are needed in order to accomplish this.
— 19:13
Case paths are also used in our parser-printer library. It allows you to parse a nebulous blob of input data into an enum, and allows you to print an enum back into data. And again this only works thanks to case paths’ extract and embed functionality.
— 19:29
Case paths can also be used in vanilla SwiftUI applications for driving navigation off of an enum that describes all possible destinations. This makes it possible to concisely model your domains and completely omit invalid states.
— 19:43
And there are even more use cases out there.
— 19:45
And because case paths are so useful, we decided it was necessary to extract out into its own library, and it’s one of our most cloned repos. Every two weeks it has over 60,000 clones from over 6,000 unique cloners.
— 20:00
We can even import it in this project: import CasePaths
— 20:04
And have Xcode add it as a dependency…
— 20:08
And we will see that this library has the CasePath type defined exactly as we sketched out a moment ago.
— 20:32
However, this library has a lot more than just this simple type. The best part of key paths is that the Swift compiler generates them for us automatically. We don’t need to write out the explicit getter and setter closures, because that would be an absolute pain.
— 21:02
But if our case paths library only provide this CasePath type, then that is exactly what you would have to do for each of your enums and each case of those enums. For example, a case path for the loaded case of the Loading enum would have to be constructed like this: let loadedCasePath = CasePath( embed: Loading.loaded, extract: { guard case let .loaded(value) = $0 else { return nil } return value } )
— 21:32
And that would be a real pain. No one would want to maintain this boilerplate.
— 21:39
So, that’s why the library ships a tool that allows one to derive a case path from just the embed function of a case, which comes for free thanks to how Swift designed enums: let loadedCasePath = CasePath(Loading.loaded)
— 22:05
And if you have an appetite for it, we even ship a prefix operator with the library to shorten this a little bit to look more similar to key path syntax: let loadedCasePath = /Loading.loaded
— 22:22
It uses a forward slash instead of a backward slash since case paths are kind of like the “dual” concept to key paths.
— 22:37
Once you construct a case path you can then use it to perform extractions and embeddings. For example, we can use a case path to extra the loaded case from a Loading value: let value = Loading.loaded("Hello") (/Loading.loaded).extract(from: value) // "Hello" (/Loading.inProgress).extract(from: value) // nil
— 23:23
And you could embed a value into Loading using the case path: (/Loading.loaded).embed("Hello") // .loaded("Hello")
— 23:33
You would of course never actually write code like this in practice, just as you would never subscript into a concrete value to get and set with a key path. Case paths are far more useful when used in generic algorithms, which is also true of key paths.
— 23:54
The case paths library even ships with one such generic algorithm, and it’s called modify . It allows you to open up a particular case of an enum to get access to its associated value, perform a mutation on that data, and then package it back up in the enum.
— 23:59
For example, if we wanted to modify the value we defined above we could first make it mutable: var value = Loading.loaded("Hello")
— 24:05
…and then use the modify method to mutate the string held inside the loaded case: try (/Loading.loaded).modify(&value) { $0 += "!!!" }
— 24:37
It works, but it’s also a little awkward. I kinda reads backwards. We specify the case we want to modify first, and then we specify the value whose case we want to modify.
— 25:09
Alternatively one would have to write this code as follows: if case let .loaded(string) = value { value = .loaded(string + "!!!") }
— 25:26
This is more verbose, and repeats loaded twice, which means it could be easy to get wrong if we are ever dealing with an enum that has the same type of associated data in two cases.
— 25:54
And this method only works thanks to the embed and extract functionality of case paths. We can jump to the source code to see: guard var value = self.extract(from: root) else { throw ExtractionFailed() } let result = try body(&value) root = self.embed(value) return result …that first modify tries to extract the associated value from the enum. If that succeeds it applies the transformation to that data, and then mutates the enum by embedding the new associated value. So we can see clearly that both the embed and extract are necessary to implement such a helper.
— 26:10
While this modify method is handy, it barely scratches the surface of what case paths are capable of. We make extensive use of case paths in our popular library, the Composable Architecture . For example, there is a higher-order reducer in the library called Scope . It allows you to run a child reducer in the context of a parent domain, and it does this by using a key path to chisel away a bit of child state from the parent state, and a case path to isolate the child actions from the parent actions. Both the extract and embed of the case path are necessary for this.
— 26:41
First we try to extract a child action from the parent action: guard let childAction = self.toChildAction .extract(from: action) else { return .none }
— 26:50
And if that succeeds, we later use embed any child actions emitted from the child effect into the parent action domain: return self.child .reduce(into: &childState, action: childAction) .map { self.toChildAction.embed($0) }
— 27:11
This scoping operation is incredibly powerful, and it gets its power thanks to case path’s ability to extract and embed cases in an enum, as well as key path’s ability to get and set fields in a struct.
— 27:23
We also use case paths in our SwiftUINavigation library , which adds a whole suite of tools to vanilla SwiftUI that allow you to model your domains more concisely using enums. The most foundational tool provided in the library is a binding method that allows you to transform a binding of an enum into a binding of one of its cases: public func case<Enum, Case>( _ casePath: CasePath<Enum, Case> ) -> Binding<Case?> where Value == Enum? { .init( get: { self.wrappedValue.flatMap(casePath.extract(from:)) }, set: { newValue, transaction in self.transaction(transaction) .wrappedValue = newValue.map(casePath.embed) } ) }
— 27:49
In order to do this we crucially need the extract and embed functionality of case paths. It could not be done otherwise.
— 27:56
And built on the back of this simple binding transformation is a whole suite of tools. We have APIs that mimic SwiftUI’s alert , sheet , fullScreenCover view modifiers, and more, but they allow you to drive navigation from an enum rather than a simple boolean. This can be incredibly powerful and allows you to more concisely model your domains, making a whole class of bugs completely impossible.
— 28:21
We even use case paths in our parser-printer library .
— 28:27
This library gives you a variety of tools for describing how to parse unstructured data into well-structured data. And if you craft your parser carefully enough, it will even have the ability to do the opposite: it can print the structured data back into its unstructured format.
— 28:44
Case paths naturally become parser printers by using their embed and extract functionality: extension CasePath: Conversion { @inlinable public func apply(_ input: Value) -> Root { self.embed(input) } @inlinable public func unapply(_ output: Root) throws -> Value { guard let value = self.extract(from: output) else { throw ConvertingError( """ case: Failed to extract \(Value.self) from \ \(output). """ ) } return value } }
— 28:54
This allows you to parse a string into an enum, and then inversely print that enum back into a string. We won’t show this off in detail, but we highly encourage our views to watch our past episodes on parser-printers if they are interested in these ideas. The problems with case paths
— 29:09
So, case paths are incredibly handy for writing powerful, generic algorithms over the shape of enums. We’ve just shown 3 very different uses for case paths: a state management library, a navigation library, and a parser-printer library.
— 29:24
And if you are building a library where you expect your users to model their domains with enums, and you want to provide them with generic algorithms that work for their domain, then you will inevitably need to use case paths. There’s just no way around it. Stephen
— 29:36
So, this all seems great, but all is not well in the land of case paths. There are problems with case paths as they are designed today. None of them are too significant, and it’s better than having nothing at all, but it is a “death by 1,000 paper cuts” kind of situation. They are annoying enough that we should look for solutions, and macros can hopefully fill in the gap.
— 29:56
Let’s take a look at some of those problems so that we can see just how amazing it is that we can solve these problems with macros.
— 30:03
The first problem we will look at is the ergonomics of case paths. Because case paths are not a part of the Swift language, there are a lot of things that don’t work the say you’d expect. For example, autocomplete in Xcode and type inference.
— 30:15
Let’s open up the Composable Architecture project so that we can see how things break down. In the library we have a demo application called “SyncUps” that is a port of some sample code that Apple released a few years ago, but rebuilt in the Composable Architecture.
— 30:30
Let’s search that project for “action: “ and we will find a bunch of places we use case paths. For example, in the root app feature we use the Scope operator we discussed a moment ago to run a child feature’s logic and behavior inside the context of the great application: Scope(state: \.syncUpsList, action: /Action.syncUpsList) { SyncUpsList() }
— 30:54
The first argument of Scope is a key path from the parent state to the child state. And because key paths are fully supported in the Swift language, we get all types of affordances that we have come to expect. The fact that we can leave off root of the key path is huge. Swift can infer that type automatically from the surrounding context, and so we do not have to write this: state: \State.syncUpsList
— 31:21
And we can even use autocomplete by hitting “ESC” while the cursor is on the . of the key path in order to see all of the other properties we could choose.
— 31:31
Case paths have none of these affordances. Because the / operator is a custom operator we defined in order to hack in some similarity between key paths and case paths, you do not get to leave off the root type: action: /.syncUpsList
— 31:44
This does not work. This means you are forced to write out the full root type, and only then do you get access to autocomplete.
— 31:54
This is a really unfortunate asymmetry in this code. It would be really amazing if we could use key path syntax to describe case paths like this: Scope(state: \.syncUpsList, action: \.syncUpsList) { … }
— 32:07
Then we would get type inference and autocomplete for free. This will be one of our goals for improving case paths with macros.
— 32:14
The next downside to case paths as they are implemented today is that they heavily rely on runtime reflection in order to make them as seamless as possible. The / operator is only possible thanks to reflection, and we can even jump to EnumReflection.swift to see 650 lines of messy reflection code.
— 33:11
It works 99.9% of the time, but over the years a small number of issues have popped up where this reflection code will fail, especially around enum payloads that contain closures and existentials. Ideally we would not have to deal with any of this code, and macros will make that possible.
— 33:28
The next gotcha we want to mention is related to dynamic member lookup. As we saw earlier, dynamic member lookup is what allows you to perform dot-chaining onto certain expressions to access fields that don’t actually exist on the value you are chaining onto, but rather exist on some underlying, wrapped value.
— 33:55
This gives you a very short and concise syntax for deriving bindings from other bindings, and it’s all thanks to the power of dynamic member lookup.
— 34:03
We would like to have something similar for enums too. It would be amazing if you could dot-chain onto a binding of an enum in order to derive a binding of each of the enum’s cases. This would be incredibly powerful, and would clean up a lot of code that makes use of our SwiftUINavigation library.
— 34:16
Let’s open up that project…
— 34:18
And in this project there is a demo application called “Inventory” which is a small, simple app that demonstrates a few common navigation patterns.
— 34:25
In the Item.swift file there is a fun demonstration of using case paths to deriving bindings to the different cases of an enum. If we run the preview we will see that an inventory item can be edited. If the item is in stock, we can change its quantity, but if we mark it as out of stock, we can then control whether or not it is on backorder.
— 35:05
We want to model this data in the most concise way possible, which means using an enum: enum Status: Equatable { case inStock(quantity: Int) case outOfStock(isOnBackOrder: Bool) }
— 35:11
But unfortunately SwiftUI does not come with the tools necessary to derive bindings to each of these enums cases so that they can be handed to the Stepper and Toggle components.
— 35:22
Well, our SwiftUI Navigation library comes with these tools, and we can use them like so: Switch(self.$item.status) { CaseLet(/Item.Status.inStock) { $quantity in Section(header: Text("In stock")) { Stepper("Quantity: \(quantity)", value: $quantity) Button("Mark as sold out") { withAnimation { self.item.status = .outOfStock( isOnBackOrder: false ) } } } .transition(.opacity) } CaseLet(/Item.Status.outOfStock) { $isOnBackOrder in Section(header: Text("Out of stock")) { Toggle("Is on back order?", isOn: $isOnBackOrder) Button("Is back in stock!") { withAnimation { self.item.status = .inStock(quantity: 1) } } } .transition(.opacity) } }
— 35:30
We are able to “switch” on a binding to the status enum with the Switch view, and then further transform that enum into each of its cases with the CaseLet view. When the case is recognized, it derives a binding to that case, which we can then use for the Stepper and Toggle .
— 35:56
So, this gets the job done, but it’s also super verbose. Wouldn’t it be better to forgo the Switch view for a native switch statement? switch self.item.status { }
— 36:16
And wouldn’t it be better if could just use dynamic member lookup in order to further dot-chain onto the $item.status binding to derive a binding for the inStock case? case .inStock: self.$item.status.inStock
— 36:27
That would need to be an optional binding to an integer: self.$item.status.inStock // Binding<Int>? …since matching against the .inStock case could fail. But we can then .map on it as an optional in order to get access to the underlying binding: self.$item.status.inStock.map { $quantity in }
— 36:37
And then we can put all of the view code inside this map: Binding(self.$item.status.inStock).map { $quantity in Section(header: Text("In stock")) { Stepper("Quantity: \(quantity)", value: $quantity) Button("Mark as sold out") { withAnimation { self.item.status = .outOfStock(isOnBackOrder: false) } } } }
— 36:41
And we could do the same for the other case: self.$item.status.outOfStock.map { $isOnBackOrder in Section(header: Text("Out of stock")) { Toggle("Is on back order?", isOn: $isOnBackOrder) Button("Is back in stock!") { withAnimation { self.item.status = .inStock(quantity: 1) } } } }
— 37:04
So, if we had dynamic member lookup for enum cases then we could get rid of two entire concepts, that of the Switch view and CaseLet view, and you would be able to write SwiftUI code in a more natural way.
— 37:22
And there’s more examples of this in the case study 09-Routing.swift where we will find a bunch of repetitive code like this: .alert( unwrapping: self.$destination, case: /Destination.alert ) { action in … } … .confirmationDialog( unwrapping: self.$destination, case: /Destination.confirmationDialog ) { action in … } … .navigationDestination( unwrapping: self.$destination, case: /Destination.link ) { $count in … } … .sheet( unwrapping: self.$destination, case: /Destination.sheet ) { $count in … }
— 37:54
This code is necessary to drive navigation from an enum. We first point the SwiftUI API at a binding of an optional enum, and then further describe the case of that enum that should drive navigation.
— 38:09
Wouldn’t it be amazing if we could write this code in a more concise, dot-syntax style? .alert(unwrapping: self.$destination.alert) { action in … } … .confirmationDialog( unwrapping: self.$destination.confirmationDialog ) { action in … } … .navigationDestination( unwrapping: self.$destination.link ) { $count in … } … .sheet(unwrapping: self.$destination.sheet) { $count in … }
— 38:30
That would make properly modeling your navigation domains even easier, and so there would be no excuse to not do it!
— 38:36
The next problem with case paths is that, unlike key paths, they are neither equatable nor hashable. That may not seem that important, and you may even be surprised to learn that key paths conformed to those protocols to begin with, but it can be incredibly powerful.
— 38:48
Key paths being hashable means you can use them as the key of a dictionary and store them in sets. This allows you to store data in a dictionary associated with each field on a type. In fact, the new Observation framework in Swift 5.9 makes extensive use of this fact. If we open that code base and search for “keypath” we will see there are places where key paths are being held in sets : internal var properties: Set<AnyKeyPath>
— 39:10
…and in dictionaries : private var lookups = AnyKeyPath : Set<Int>
— 39:11
…and this is because the Observation framework needs to keep track of who accessed what properties of a type. If key paths weren’t Hashable this kind of simple, clear code would be much more difficult, if not impossible.
— 39:22
Let’s take one more look at where case paths result in super verbose code.
— 39:26
Let’s hop back over to the Composable Architecture code base, and go to the SyncUpsListView . In that view we will find the following code for showing a sheet: .sheet( store: self.store.scope( state: \.$destination, action: { .destination($0) } ), state: /SyncUpsList.Destination.State.add, action: SyncUpsList.Destination.Action.add ) { store in
— 39:47
It’s intense, but it is also very powerful. This is showing how to drive a sheet from a single piece of enum state so that when that enum changes to the add case the sheet is presented, and when it switches away from that case, the sheet is dismissed.
— 40:43
However, there is of course a lot to not like about this code. Not only does this single method take 3 arguments, but two of them do not get the benefits of type inference or autocomplete. You have to spell out the entire root type in order to see what your choices are. Wouldn’t it be better if we could shorten the state and action parameters? .sheet( store: self.store.scope( state: \.$destination, action: { .destination($0) } ), state: \.add, action: { .add($0) } ) { store in
— 41:25
And this would already be an improvement, but it allows us to mention something that isn’t a problem with case paths per se, but just shows how we can start envisioning all new tools with macro generated case paths that were previously impossible.
— 41:39
Well, what if I were to tell you that thanks to the power of macros and case paths, all of this code could be shortened to just: .sheet( store: self.store.scope(#feature(\.destination.add)) )
— 42:09
That alone is enough to generate all of the other code. That sounds pretty incredible, and it is absolutely possible. The solution
— 42:15
So, we’ve now see all the current problems with case paths. They don’t play nicely with autocomplete and type inference, they depend on runtime reflection in order to be somewhat usable, they don’t work with dynamic member lookup, and just overall their lack of ergonomics and deep integration with the Swift language are preventing us from seeing their full potential. Brandon
— 42:34
So, let’s start fixing all of these problems, and of course macros are going to play a huge part in doing this. However, in order to fix all the problems we have just outlined we are not going to be able to approach this in a completely naive way. It is not enough to literally generate a case path for each case of an enum. If we want to be able to use key path-like syntax but for enum cases, we are going to have to employ some tricks.
— 42:57
So let’s get started.
— 43:00
Since the Composable Architecture is one of the biggest users of case paths, let’s start in that project so that we can see how to how can slowly improve the situation.
— 43:08
Recall that we took a look at the following usage of a case path: Scope(state: \.syncUpsList, action: /Action.syncUpsList) { SyncUpsList() }
— 43:16
…and theorized that it would be much nicer if we could use key path syntax to grab the Action ’s case path: Scope(state: \.syncUpsList, action: \.syncUpsList) { SyncUpsList() }
— 43:23
Let’s see what it takes to make that a reality.
— 43:25
If we approached this problem in the most naive way, we may be tempted to generate computed properties on this type for each case that allow you to get an set based on the associated value. If we did so just for the syncUpsList case: enum Action: Equatable { case path(StackAction<Path.State, Path.Action>) case syncUpsList(SyncUpsList.Action) var syncUpsList: SyncUpsList.Action? { get { guard case let .syncUpsList(value) = self else { return nil } return value } set { guard let newValue = newValue else { return } self = .syncUpsList(newValue) } } }
— 44:36
That would certainly give you a writable key path for the case: let _ = \Action.syncUpsList
— 44:52
But there’s a few things not correct about this. First of all, this is a writable key path into an optional SyncUpsList.Action , not an honest SyncUpsList.Action : let _: WritableKeyPath<Action, SyncUpsList.Action?> = \Action.syncUpsList
— 45:23
That means you can do nonsensical things like set nil : var syncUpsListAction = Action.syncUpsList( .addSyncUpButtonTapped ) let _ = syncUpsListAction.syncUpsList = nil
— 45:31
This has no choice but to be a no-op, and so why should we even allow it?
— 45:39
At the end of the day, we really do need the concept of the CasePath type since it expresses the two fundamental operations for enums: extraction and embedding. We cannot use key paths to completely replace the concept of case paths, but we hope that we can use key path syntax for generating case paths.
— 46:00
And the second thing wrong with this is that we will be littering people’s enums with a property for every single case, and that may not be what people want. It will pollute their autocomplete and just overall has the chance of being very confusing.
— 46:17
So, rather than generating computed properties directly in people’s enums, we will instead create a little inner type that will house all of the case paths for the enum: enum Action: Equatable { case path(StackAction<Path.State, Path.Action>) case syncUpsList(SyncUpsList.Action) struct Cases { } }
— 46:33
And the case paths will be held in this type in a very specific way. Recall that we want to be able to leverage key path syntax for referring to case paths without literally using key paths. We can’t use key paths to replace case path functionality, but that doesn’t mean we can’t use key paths to generate case paths.
— 46:51
This means we will house the case paths inside Cases as stored properties: enum Action: Equatable { … struct Cases { let syncUpsList = CasePath<Action, SyncUpsList.Action>( embed: Action.syncUpsList, extract: { guard case let .syncUpsList(value) = $0 else { return nil } return value } ) } }
— 47:30
So, we are generating a case path for each case of the enum, and because its an instance property on the type we even get a key path: let _ = \Action.Cases.syncUpsList
— 47:56
It’s a strange key path since it lives inside Cases and its type is: KeyPath<Action.Cases, CasePath<Action, SyncUpsList.Action>>
— 48:09
…but we will soon see that this is going to be super handy, and all of this weirdness can be hidden away.
— 48:16
Now, because this key path is of such a strange shape, we of course cannot simply pass it to the Scope reducer: Scope( state: \.syncUpsList, action: \Action.Cases.syncUpsList ) { SyncUpsList() } And if that doesn’t work then certainly we can’t even leverage type inference to shorten it: Scope(state: \.syncUpsList, action: \.syncUpsList) { SyncUpsList() } So, we don’t seem to have accomplished much yet.
— 48:34
However, we just need to look at the problem from a slightly different angle. While this key path shape does seem bizarre: KeyPath<Action.Cases, CasePath<Action, SyncUpsList.Action>>
— 48:43
…what if we made this the shape that generic algorithms wanting to operate over enums should expect? Rather than being given a true case path, you are given a key path that is capable of plucking a case path out of the Cases type.
— 49:01
Let’s see what such a generic algorithm would look like. In fact, let’s not jump straight to full generality just yet, and instead focus just on the AppFeature domain. Let’s see what it would take to define a new kind of scoping reducer such that this syntax actually works: Scope(state: \.syncUpsList, action: \.syncUpsList) { SyncUpsList() }
— 49:15
We’ll call it NewScope : struct NewScope: Reducer { }
— 49:22
Now, you may not be super familiar with the Composable Architecture and its concepts such as reducers, but that is ok. You do not need to know how these tools actually work, we are just wanting to see how an existing tool can be adapted to use this new, strange case/key path contraption. We will just be following the compiler’s lead trying to get the types to match up and will not be using any real knowledge of Composable Architecture to accomplish this.
— 49:50
Now, we can see from the call site of Scope that it definitely takes 3 arguments: Scope(state: \.syncUpsList, action: \.syncUpsList) { SyncUpsList() }
— 49:56
A state key path, an action key path that we now know should project into a case path, and then a trailing closure for a child reducer. So, let’s add those properties to this NewScope .
— 50:12
We’ll start with the state key path, which will project from the AppFeature ’s state down into some child state: struct NewScope: Reducer { let state: WritableKeyPath<AppFeature.State, <#???#>> … }
— 50:20
Which means we need a generic on NewScope to represent the child domain. We could introduce separate ChildState and ChildAction generics to accomplish this, but later on we are also going to need a Child reducer, and that is naturally endowed with State and Action , so might as well just do that from the beginning: struct NewScope<Child: Reducer>: Reducer { let state: WritableKeyPath<AppFeature.State, Child.State> … }
— 50:28
Next we’ll need the action key path that will have that very strange shape of projecting from the Cases type down into a case path: struct NewScope<Child: Reducer>: Reducer { … let action: KeyPath< AppFeature.Action.Cases, CasePath<AppFeature.Action, Child.Action> > … }
— 50:55
Looks bizarre, but let’s roll with it.
— 50:57
And the final argument to hold onto is a closure that returns the child reducer to run: struct NewScope<Child: Reducer>: Reducer { … let child: () -> Child … }
— 51:16
And just to get this compiling we can quickly stub out the body of the reducer by returning an “empty” reducer, which is just a reducer that executes no logic or behavior: var body: some ReducerOf<AppFeature> { EmptyReducer() }
— 51:36
Now our NewScope reducer is compiling, and let’s see if we can use it down in our app feature: NewScope(state: \.syncUpsList, action: \.syncUpsList) { SyncUpsList() }
— 51:46
Amazingly this compiles! And we now get type inference and autocomplete for the action argument. If we put the cursor at the . and press the escape key we will get a list of all the various case paths we can choose from.
— 51:52
Right now we just hard coded a single case path for the syncUpsList case because that’s all we needed, but let’s quickly copy-and-paste that one and adapt it for the path case: struct Cases { … let path = CasePath< Action, StackAction<Path.State, Path.Action> >( embed: Action.path, extract: { guard case let .path(value) = $0 else { return nil } return value } ) }
— 52:18
Now when we bring up the autocomplete popover again we will see we have two choices: syncUpsList and path .
— 52:24
So, this is looking great so far, but we didn’t actually implement the logic in the NewScope reducer. What does it take to actually use this new, strange key path / case path hybrid thing?
— 52:42
Well, we can start by using the key path aspect of it in order to subscript into Cases and procure a case path: var body: some ReducerOf<AppFeature> { let actionCasePath = AppFeature.Action.Cases()[ keyPath: self.action ] EmptyReducer() }
— 53:10
This also looks strange. We are constructing a Cases just so that we can subscript into it and then discard the Cases . However, it does compile, and does give us access to a proper case path from the AppFeature.Action domain into the Child.Action domain: let actionCasePath: CasePath< AppFeature.Action, Child.Action >
— 53:26
And now that we have a proper, honest case path, we can use it exactly as we would have before. We can use its extract functionality to try extracting a child action from a parent action, and we can use its embed functionality to embed child effect actions back into the parent domain. But we don’t need to actually do any of that manually because that’s exactly what the original Scope reducer did for us. So let’s just defer to it: var body: some ReducerOf<AppFeature> { let actionCasePath = AppFeature.Action.Cases()[ keyPath: self.action ] Scope( state: self.state, action: actionCasePath, child: self.child ) }
— 53:55
This compiles, and we now have our first generic algorithm that operates over this strange key path / case path hybrid concept. And it allows us to use key path syntax at the call site, but secretly under the hood that key path is just being used to give us access to case paths stored in that Cases type.
— 54:12
However, this “generic” algorithm isn’t as generic as it could be. It’s still rooted in the very concrete concept of the AppFeature domain: struct NewScope<Child: Reducer>: Reducer { let state: WritableKeyPath<AppFeature.State, Child.State> let action: KeyPath< AppFeature.Action.Cases, CasePath<AppFeature.Action, Child.Action> > let child: () -> Child … }
— 54:19
Ideally this could be generic over the parent state and actions, just like the original Scope is: public struct Scope< ParentState, ParentAction, Child: Reducer >: Reducer { … }
— 54:30
But we can’t simply introduce those generics and start using them in place of AppFeature.State and AppFeature.Action : struct NewScope< ParentState, ParentAction, Child: Reducer >: Reducer { let state: WritableKeyPath<ParentState, Child.State> let action: KeyPath< ParentAction.Cases, CasePath<ParentAction, Child.Action> > let child: () -> Child var body: some Reducer<ParentState, ParentAction> { let actionCasePath = ParentAction.Cases()[ keyPath: self.action ] Scope( state: self.state, action: actionCasePath, child: self.child ) } }
— 55:01
This does not compile because generically Swift does not know that ParentAction has this Cases type.
— 55:12
We need some way to generically constrain this NewScope operator to only work when we know for sure that the ParentAction type has an inner Cases type that holds case paths.
— 55:25
This sounds like the perfect job for a protocol, which we will name CasePathable : protocol CasePathable { associatedtype Cases static var cases: Cases { get } }
— 55:51
It simply expresses the idea of a type that can statically provide some type that houses all of its case paths.
— 55:59
With this protocol we can now constrain our NewScope reducer to only work on “case-pathable” parent actions: struct NewScope< ParentState, ParentAction: CasePathable, Child: Reducer >: Reducer { let state: WritableKeyPath<ParentState, Child.State> let action: KeyPath< ParentAction.Cases, CasePath<ParentAction, Child.Action> > let child: () -> Child var body: some Reducer<ParentState, ParentAction> { let actionCasePath = ParentAction.cases[ keyPath: self.action ] Scope( state: self.state, action: actionCasePath, child: self.child ) } }
— 56:22
This reducer compiles, but the rest of the app does not because we need to make AppFeature ’s Action conform to the CasePathable protocol: enum Action: Equatable, CasePathable { … }
— 56:39
And to satisfy the requirements we need to provide a static let for all of the case paths: enum Action: Equatable, CasePathable { … static let cases = Cases() }
— 56:48
And now everything compiles, and we truly have a generic algorithm that is operating with our newfound, yet still kinda bizarre, key/case path hybrid.
— 57:01
Let’s just do one quick thing to lessen the bizarreness just a tad. We can define a type alias for key paths into case paths that works specifically for CasePathable roots like so: typealias CaseKeyPath<Root: CasePathable, Value> = KeyPath<Root.Cases, CasePath<Root, Value>>
— 57:27
And now NewScope can more simply hold onto a CaseKeyPath rather than the current abomination: let action: CaseKeyPath<ParentAction, Child.Action> Next time: case key paths for free Brandon
— 57:39
So, we’ve done some pretty interesting things just now. We decided to house all of the case paths for an enum inside an inner Cases type, that way we don’t pollute the enum with a bunch of superfluous properties that will clutter autocomplete and the documentation of your types. And then we flipped the notion of “case path” on its head by instead thinking of it as a key path that locates a case path inside that Cases type. That instantly gave us key path syntax to procure case paths, and gave us type inference and nice autocomplete for free! Stephen
— 58:13
But of course what we have done so far is not how anyone would want to write their applications. No one is going to define the Cases type for each one of their enums and then fill those types with properties for each case in the enum. That is a long, laborious process that is very easy to get wrong.
— 58:31
And this is a perfect use case for a macro. We should be able to annotate our enums with some kind of @CasePathable macro, and have all of that code generated for us automatically. This will be the first truly useful macro that we have built on Point-Free, and it is not a trivial one. There are some tricky aspects to it, and luckily we have our new MacroTesting library to help us each step of the way.
— 58:53
So, let’s see what it takes to write a macro to generate all of this case path boilerplate automatically…next time! Downloads Sample code 0257-macro-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 .