Video #70: Composable State Management: Action Pullbacks
Episode: Video #70 Date: Aug 19, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep70-composable-state-management-action-pullbacks

Description
Turns out, reducers that work on local actions can be pulled back to work on global actions. However, due to an imbalance in how Swift treats enums versus structs it takes a little work to implement. But never fear, a little help from our old friends “enum properties” will carry us a long way.
Video
Cloudflare Stream video ID: ad59d5b56dbc393d214467f1443a6498 Local file: video_70_composable-state-management-action-pullbacks.mp4 *(download with --video 70)*
References
- Discussions
- swift-enum-properties repo
- Category Theory
- Composable Reducers
- Elm: A delightful language for reliable webapps
- Redux: A predictable state container for JavaScript apps.
- Pullback
- 0070-composable-state-management-action-pullbacks
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
So we are now getting pretty close to accomplishing yet another architectural problem that we set out to solve at the beginning of this series of episodes. We stated that we wanted to be able to build large complex applications out of simple, composable units.
— 0:22
We can now do this with our reducers and the state they operate on. We can write our reducers so that they operate on just the bare minimum of state necessary to get the job done, and then pull them back to fit inside a reducer that is much larger and operates on a full application’s state.
— 0:35
Ideally we’d even want those simple, composable units to be so isolated that we may even be able to put them in their own module so that they could easily be shared with other modules and apps.
— 0:45
This is getting pretty exciting! But, there’s still a problem. Even though our reducers are operating on smaller pieces of data, they still know far too much about the larger reducer they are embedded in, particularly they can listen in on every single app action.
— 1:05
It sounds like we need to repeat the same story for actions that we have for state. Focusing a reducer’s actions
— 1:14
Let’s take another look at our counterReducer : func counterReducer(state: inout Int, action: AppAction) -> Void { switch action { case .counter(.decrTapped): state -= 1 case .counter(.incrTapped): state += 1 default: break } }
— 1:17
Now it’s nice that this reducer only operates on a single integer. That really helps my mental model of what this reducer is trying to accomplish because I know it can’t really do too much. It only has this one single integer it can mutate after all.
— 1:26
However, notice that it also takes in the full entirety of the application’s actions.
— 1:31
We should know that something’s wrong because we have a default case in our switch statement. This means if we add a new action to our CounterAction enum we will not get a compiler error and will be silently ignore that action in our reducer.
— 1:49
One way to address this shortcoming is to first extract the counter action before switching over it exhaustively: func counterReducer(state: inout Int, action: AppAction) -> Void { switch action { case let .counter(action): switch action { case .decrTapped: state -= 1 case .incrTapped: state += 1 } default: break } }
— 2:11
But this isn’t ideal because it increases nesting and noise.
— 2:19
So, let’s refactor this reducer so that it only takes in the actions it cares about: func counterReducer( state: inout Int, action: CounterAction ) -> Void { switch action { case .decrTapped: state -= 1 case .incrTapped: state += 1 } }
— 2:48
That was easy enough, it made the code a lot shorter and we no longer need the default case anymore. It now has the narrowest focus it could possibly have on actions and state and could even be extracted to a module. And that’s a powerful thing! For example, a newcomer to a code base could look at a reducer in its own module and know that it can’t possibly touch anything in the app target. Enums and key paths
— 3:40
However, we now have a compiler error in our app reducer: let appReducer = combine( pullback(counterReducer, value: \.count), primeModalReducer, pullback(favoritePrimesReducer, value: \.favoritePrimesState) ) Type of expression is ambiguous without more context
— 3:46
Although the counterReducer speaks the same language as the other reducers when it comes to state, it no longer speaks the same language of actions. The counter reducer only understands counter actions and the other reducers understand the full universe of app actions, and so we can no longer combine these reducers together.
— 4:15
So, the question is: how can we take a reducer that only understands local actions and transform it into one that understands global actions?
— 4:22
This is very similar to problem we had with reducers and state, since we wanted to transform reducers that worked on local state into ones that work on global state. In that situation we found the solution was to pullback along a key path that goes from the global state down into the local state. But what is the corresponding solution over in the action world?
— 4:42
Our actions are enums, and enums have no concept of key paths, at least not as defined by Swift and not provided automatically by the Swift compiler. However, that shouldn’t stop us from exploring what the corresponding concept of key paths for enums could look like, and see if that might solve our problem.
— 5:00
If we were to distill the essence of key paths into a single package, it might look something like this: struct _KeyPath<Root, Value> { let get: (Root) -> Value let set: (inout Root, Value) -> Void }
— 5:08
This is not at all how key paths are actually implemented in Swift, and we’ve prefixed this with an underscore to make it clear that this is just a thought experiment right now. But the core essence of key paths is that they give you a means to “get” a value out of a root, and they allow you to set a value inside a root, thereby giving you a new root that has been changed. These two operations are basically the most general operations you can perform on a struct. You can either pluck out a field from the struct, or you can set a new value in a field on a struct.
— 5:48
Enums also have two very fundamental operations that can be performed on them, and they are pretty similar to the get and set of key paths. For some enum type, you can take a value and embed it into one of the cases of the enum, or you can take a value of the enum and try to extract out the associated data in one of its cases.
— 6:09
For example, from our AppAction enum. We can take a value from the associated data of a case and stick it into the enum: AppAction.counter(CounterAction.incrTapped)
— 6:19
This is kind of like a setter: we are taking a value and embedding it inside the AppAction enum.
— 6:26
We also have what is kind of like a getter: we can take an enum value and try to extract a value out of a particular case: let action = AppAction.favoritePrimes(.deleteFavoritePrimes([1])) let favoritePrimesAction: FavoritePrimesAction? switch action { case let .favoritePrimes(action): favoritePrimesAction = action default: favoritePrimesAction = nil }
— 7:22
While this is super verbose, it resembles a getter-like operation in which we could pattern match and extract out an optional associated value.
— 7:40
So these are the two operations we can do on enums, and we can do it for each case of an enum. If Swift supported enum key paths we could have these two operations packaged up into a new type, perhaps like this: struct EnumKeyPath<Root, Value> { let embed: (Value) -> Root let extract: (Root) -> Value? }
— 8:25
And perhaps the Swift compiler could automatically create enum key paths for us, one for each case of an enum, and maybe we could access them via the same syntax: // \AppAction.counter // EnumKeyPath<AppAction, CounterAction> Enum properties
— 8:58
Now, even though Swift does not give us this feature today, it turns out that we can get extremely close to having this if we just do a little bit of upfront work. And in fact, we had an entire series of episodes on this very topic.
— 9:17
A few months ago, we dedicated a few episodes to the idea that structs and enums are really just two sides of the same coin, that is, they are fundamentally connected concepts. This led us to see that many features that one concept has the other will naturally have too. However, sometimes Swift prefers structs over enums by giving us powerful features that have no corresponding version over in the enum world.
— 9:46
In particular: properties and key paths. Structs have very simple data access via dot syntax, which means you can access a field inside a struct very easily by just doing “.” and then the name of your field. Enums have no such affordance. If you want to get at the data inside an enum you have no choice but to switch on the enum, pattern match on the case that you care about, and then grab the associated data in that case. Further, every property on a struct gets a compiler generated key path, which is like a little getter/setter pair that can unlock all types of interesting things. There is no such thing for enums.
— 10:29
This is a pretty big imbalance between the two concepts. It makes it seem as if Swift prefers structs over enums, even though one is no more important than the other. So, in the next episode of that series we set out to remedy this by introducing the concept known as “ enum properties .” These are computed properties that are defined on enums, one for each case of the enum. It basically bundles up the work that we did in an ad hoc fashion above into a nice, consistent package.
— 11:00
For example, for the AppAction enum it would look like this: enum AppAction { case counter(CounterAction) case primeModal(PrimeModalAction) case favoritePrimes(FavoritePrimesAction) var counter: CounterAction? { get { guard case let .counter(value) = self else { return nil } return value } } var primeModal: PrimeModalAction? { get { guard case let .primeModal(value) = self else { return nil } return value } } var favoritePrimes: FavoritePrimesAction? { get { guard case let .favoritePrimes(value) = self else { return nil } return value } } }
— 11:45
With these properties we get instance access to the associated data of any case in the AppAction enum, which gives app actions a lot of the same ergonomics as the app state struct.
— 11:54
Although these enum properties are incredibly useful they would be quite a pain to write from scratch and maintain over time. So that’s why we then devoted three additional episodes to exploring the SwiftSyntax library from Apple so that we could build a command line tool that generates these properties for us, and inserts the code automatically into our source code. We eventually open sourced this tool, and it’s so easy to use that we can actually add it directly to this playground and have all of these enum properties automatically generated for us. So let’s do it!
— 12:22
We have added a Package.swift file to the directory where our playground currently resides, with a dependency that points to our open source swift-enum-properties repo : // swift-tools-version:4.2 import PackageDescription let package = Package( name: "StateManagement", dependencies: [ .package( url: "https://github.com/pointfreeco/swift-enum-properties.git", from: "0.1.0" ) ] )
— 12:39
And with that we can already run the generate-enum-properties executable that comes with this tool: $ swift run generate-enum-properties Generate enum properties (version 0.1.0). usage: generate-enum-properties [--help|-h] [--dry-run|-n] [<file>...] -h, --help Print this message. -n, --dry-run Don't update files in place. Print to stdout instead. --version Print the version. This prints out the usage instructions.
— 13:01
To actually invoke the tool all we have to do is point it to all the .swift files in this directory and all child directories. Let’s first delete all of the properties we have in AppAction so that we can see it work. And then run: $ swift run generate-enum-properties \ ComposableArchitecture.playground/Contents.swift Updating ComposableArchitecture.playground/Contents.swift
— 13:23
And just like that our playground code was updated with new code that provides enum properties for every single enum we have defined, and every case in that enum. For example, this property was added to AppAction : var counter: CounterAction? { get { guard case let .counter(value) = self else { return nil } return value } set { guard case .counter = self, let newValue = newValue else { return } self = .counter(newValue) } }
— 13:32
Here we’ve got the getter that we previously defined, and we’ve even got a setter which we won’t use right now, but will be handy in later episodes.
— 13:50
These properties that the tool generated for us now make enums act very similarly to structs. For example, we get easy access to plucking a value out of an enum case: let someAction = AppAction.counter(.incrTapped) someAction.counter // Optional(incrTapped) someAction.favoritePrimes // nil
— 14:49
We also now get key paths for each case of our enum: \AppAction.counter // WritableKeyPath<AppAction, CounterAction?> Pulling back reducers along actions
— 15:16
So, we now have a means of generating properties and key paths for each case of our enums, what does that give us? Well, key paths on structs is precisely what gave us the ability to pullback reducers along state. Hopefully, key paths on enums gives us the same ability, except we want to pullback reducers along actions.
— 15:53
To understand what we are trying to accomplish, let’s cook up a function signature. We want to be able to pullback reducers that know how to work with local actions to reducers that know how to work on global actions: func pullback<Value, GlobalAction, LocalAction>( _ reducer: @escaping (inout Value, LocalAction) -> Void, <#???#> ) -> (inout Value, GlobalAction) -> Void { <#???#> }
— 16:42
The question is: what are we going to pull back along? For structs it was a simple key path from global state to local state. But that isn’t quite right here because it isn’t always possible to extract a particular local action from a global action. That’s why the enum properties return optional values. So what we need is a key path into an optional local action: func pullback<Value, GlobalAction, LocalAction>( _ reducer: @escaping (inout Value, LocalAction) -> Void, action: WritableKeyPath<GlobalAction, LocalAction?> ) -> (inout Value, GlobalAction) -> Void { ??? }
— 17:24
We should be able to implement this function. Before we do that, let’s just think out loud what it is supposed to represent. We start with a reducer that operates on local actions, that is actions that are specific to say one little screen in our larger application. We also start with a key path that can extract local actions from global actions, but sometimes that fails. We want to turn this starting information into a reducer that works on global actions across the entire app. The way it will do that is when a global action comes in we will try to extract a local action from it using the key path. If that succeeds then we can pass it on through to our reducer, and if it fails we will just silently let that action go by without doing anything.
— 18:06
It’s pretty simple really, and the implementation is just as simple: func pullback<Value, GlobalAction, LocalAction>( _ reducer: @escaping (inout Value, LocalAction) -> Void, action: WritableKeyPath<GlobalAction, LocalAction?> ) -> (inout Value, GlobalAction) -> Void { return { value, globalAction in guard let localAction = globalAction[keyPath: action] else { return } reducer(&value, localAction) } }
— 18:41
And that’s about it.
— 18:42
Before we move on, let’s do something real quick to clean this up. Right now we have two versions of pullback : one that works with state and one that works with actions. Let’s combine them into just a single pullback for both: func pullback<GlobalValue, LocalValue, GlobalAction, LocalAction>( _ reducer: @escaping (inout LocalValue, LocalAction) -> Void, value: WritableKeyPath<GlobalValue, LocalValue>, action: WritableKeyPath<GlobalAction, LocalAction?> ) -> (inout GlobalValue, GlobalAction) -> Void { return { globalValue, globalAction in guard let localAction = globalAction[keyPath: action] else { return } reducer(&globalValue[keyPath: value], localAction) } }
— 20:20
How can we use this? Right now our appReducer is not compiling. We need to update these pullbacks so that they work with the new signature, which requires specify the action. The counterReducer can now be pulled back via the key path that projects app actions into counter actions: let _appReducer = combine( pullback(counterReducer, value: \.count, action: \.counter), primeModalReducer, pullback(favoritePrimesReducer, value: \.favoritePrimesState) )
— 20:39
We also need to update the pullbacks of the favoritePrimesReducer and appReducer . One simple thing we could do is pull it back along the identity key path: let _appReducer = combine( pullback(counterReducer, value: \.count, action: \.counter), primeModalReducer, pullback( favoritePrimesReducer, value: \.favoritePrimesState, action: \.self ) ) let appReducer = pullback(_appReducer, value: \.self, action: \.self)
— 21:06
This gets things compiling again, and we can see that the counter works just as before, despite its reducer being fully isolated from app state and app actions. Pulling back more reducers
— 21:40
We now have our third form of reducer composition: we are able to pull back along action key paths in order to allow our reducers to just focus on local actions that we pulled back to a world of global actions.
— 21:57
And we’ve drastically simplified the counter reducer in the process.
— 22:00
Our other reducers are still operating on global actions, so let’s fix that!
— 22:13
The favoritePrimesReducer is pulling its action back along the identity key path, which means it is still operating on the full app action enum. pullback( favoritePrimesReducer, value: \.favoritePrimesState, action: \.self )
— 22:25
We can make the reducer simpler and more specific by only working with favorite primes actions: func favoritePrimesReducer( state: inout FavoritePrimesState, action: FavoritePrimesAction ) -> Void { switch action { case let .deleteFavoritePrimes(indexSet): for index in indexSet { state.activityFeed.append( .init( timestamp: Date(), type: .removedFavoritePrime(state.favoritePrimes[index]) ) ) state.favoritePrimes.remove(at: index) } } }
— 23:02
And then we can update the pullback to go along the key path that projects app actions into favorite prime actions: let appReducer = combine( pullback(counterReducer, value: \.count, action: \.counter), primeModalReducer, pullback( favoritePrimesReducer, value: \.favoritePrimesState, action: \.favoritePrimes ) )
— 23:19
Everything still builds and runs, as before.
— 23:39
And finally we have the primeModalReducer , which is also operating with far too general of a set of actions.
— 23:57
We can make it more specific to work only on modal actions: func primeModalReducer( state: inout AppState, action: PrimeModalAction ) -> Void { switch action { case .removeFavoritePrimeTapped: state.favoritePrimes.removeAll(where: { $0 == state.count }) state.activityFeed.append( .init( timestamp: Date(), type: .removedFavoritePrime(state.count) ) ) case .saveFavoritePrimeTapped: state.favoritePrimes.append(state.count) state.activityFeed.append( .init( timestamp: Date(), type: .addedFavoritePrime(state.count) ) ) } }
— 24:21
And then we can update the app reducer like so: let _appReducer = combine( pullback(counterReducer, value: \.count, action: \.counter), pullback(primeModalReducer, value: \.self, action: \.primeModal), pullback( favoritePrimesReducer, value: \.favoritePrimesState, action: \.favoritePrimes ) ) Generic parameter ‘Action’ could not be inferred
— 24:37
We have a compiler error because we have completely isolated our reducers away from AppAction and so it can no longer be inferred. The compiler doesn’t know what kind of global action it’s pulling back to.
— 24:51
We can give it a hint by annotating our reducer function. let _appReducer: (inout AppState, AppAction) -> Void = combine( pullback(counterReducer, value: \.count, action: \.counter), pullback(primeModalReducer, value: \.self, action: \.primeModal), pullback( favoritePrimesReducer, value: \.favoritePrimesState, action: \.favoritePrimes ) )
— 25:05
The compiler’s happy and everything works!
— 25:18
We have refactored our huge reducer that worked on global state and global actions into 3 smaller reducers that operate only on the pieces of state and actions they care about. We were then able to pull back those small reducers and combine them in order to form our mega app reducer.
— 25:36
We were able to do most of this work with very very little library code. Our Store class is like 15 lines of code, and the pullback is another 4 lines or so. That is the core of our “architecture” that describes a consistent manner to apply state mutations in our application.
— 25:40
The only real downside and cost to this style of architecture is that we needed to turn to a bit of code generation to have key paths for each case of our action enums. We agree that this is not ideal, but really we are just doing code generation to make up for the imbalance between structs and enums in Swift. We consider this to be one of the more innocent uses of code generation because it is solving a serious deficiency in Swift’s data types, and hopefully someday Swift will fix this imbalance between structs and enums. Till next time
— 26:26
Now that we have the basics of our architecture in place we can start to explore things to do with it that unlock capabilities that were not even possible in the old way of making the app. There is a concept that we have discussed a number of times on Point-Free known as “higher-order constructions.” This is where you take some construction that you have been studying and lift it to a higher order by considering functions that take that object as input and return that object as output. The canonical example is “higher-order functions”, which are functions that take functions as input and return functions as output.
— 27:38
But on Point-Free we’ve also considered “higher-order random number generators”, which were functions that took our Gen type as input and returned the Gen type as output. And we’ve considered “higher order parsers”, which are functions that take parsers as input and return parsers as output. Each time you form one of these higher order constructions you gain the ability to unlock something new that the vanilla constructions could not do alone.
— 27:58
So what does a “higher order reducer” even look like? What does it mean to have a function that takes a reducer as input and returns a reducer as output? We’ll explore this next time! References Contravariance Brandon Williams & Stephen Celis • Apr 30, 2018 We first explored the concept of the pullback in our episode on “contravariance”, although back then we used a different name for the operation. The pullback is an instrumental form of composition that arises in certain situations, and can often be counter-intuitive at first sight. Note Let’s explore a type of composition that defies our intuitions. It appears to go in the opposite direction than we are used to. We’ll show that this composition is completely natural, hiding right in plain sight, and in fact related to the Liskov Substitution Principle. https://www.pointfree.co/episodes/ep14-contravariance Category Theory The topic of category theory in mathematics formalizes the idea we were grasping at in this episode where we claim that pulling back along key paths is a perfectly legimate thing to do, and not at all an abuse of the concept of pullbacks. In category theory one fully generalizes the concept of a function that maps values to values to the concept of a “morphism”, which is an abstract process that satisfies some properties with respect to identities and composition. Key paths are a perfectly nice example of morphisms, and so category theory is what gives us the courage to extend our usage of pullbacks to key paths. https://en.wikipedia.org/wiki/Category_theory Composable Reducers Brandon Williams • Oct 10, 2017 A talk that Brandon gave at the 2017 Functional Swift conference in Berlin. The talk contains a brief account of many of the ideas covered in our series of episodes on “Composable State Management”. https://www.youtube.com/watch?v=QOIigosUNGU Structs 🤝 Enums Brandon Williams & Stephen Celis • Mar 25, 2019 To understand why it is so important for Swift to treat structs and enums fairly, look no further than our episode on the topic. In this episode we demonstrate how many features of one manifest themselves in the other naturally, yet there are still some ways in which Swift favors structs over enums. 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 Enum Properties Brandon Williams & Stephen Celis • Apr 1, 2019 The concept of “enum properties” were essential for our implementation of the “action pullback” operation on reducers. We first explored this concept in episode #52 and showed how this small amount of boilerplate can improve the ergonomics of data access in enums. Note Swift makes it easy for us to access the data inside a struct via dot-syntax and key-paths, but enums are provided no such affordances. This week we correct that deficiency by defining the concept of “enum properties”, which will give us an expressive way to dive deep into the data inside our enums. https://www.pointfree.co/episodes/ep52-enum-properties Swift Syntax Command Line Tool Brandon Williams & Stephen Celis • Apr 22, 2019 Although “enum properties” are powerful, it is a fair amount of boilerplate to maintain if you have lots of enums. Luckily we also were able to create a CLI tool that can automate the process! We use Apple’s SwiftSyntax library to edit source code files directly to fill in these important properties. Today we finally extract our enum property code generator to a Swift Package Manager library and CLI tool. We’ll also do some next-level snapshot testing not only will we snapshot-test our generated code, but we’ll leverage the Swift compiler to verify that our snapshot builds. https://www.pointfree.co/episodes/ep55-swift-syntax-command-line-tool pointfreeco/swift-enum-properties Brandon Williams & Stephen Celis • Apr 29, 2019 Our open source tool for generating enum properties for any enum in your code base. https://github.com/pointfreeco/swift-enum-properties Elm: A delightful language for reliable webapps Elm is both a pure functional language and framework for creating web applications in a declarative fashion. It was instrumental in pushing functional programming ideas into the mainstream, and demonstrating how an application could be represented by a simple pure function from state and actions to state. https://elm-lang.org Redux: A predictable state container for JavaScript apps. The idea of modeling an application’s architecture on simple reducer functions was popularized by Redux, a state management library for React, which in turn took a lot of inspiration from Elm . https://redux.js.org Pullback We use the term pullback for the strange, unintuitive backwards composition that seems to show up often in programming. The term comes from a very precise concept in mathematics. Here is the Wikipedia entry: In mathematics, a pullback is either of two different, but related processes precomposition and fibre-product. Its “dual” is a pushforward. https://en.wikipedia.org/wiki/Pullback Some news about contramap Brandon Williams • Oct 29, 2018 A few months after releasing our episode on Contravariance we decided to rename this fundamental operation. The new name is more friendly, has a long history in mathematics, and provides some nice intuitions when dealing with such a counterintuitive idea. https://www.pointfree.co/blog/posts/22-some-news-about-contramap Downloads Sample code 0070-composable-state-management-action-pullbacks 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 .