Video #69: Composable State Management: State Pullbacks
Episode: Video #69 Date: Aug 12, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep69-composable-state-management-state-pullbacks

Description
So far we have pulled a lot of our application’s logic into a reducer, but that reducer is starting to get big. Turns out that reducers emit many types of powerful compositions, and this week we explore two of them: combines and pullbacks.
Video
Cloudflare Stream video ID: e2ef4ccdcdf4f501008455a6bc62c91b Local file: video_69_composable-state-management-state-pullbacks.mp4 *(download with --video 69)*
References
- Discussions
- Pullback
- Category Theory
- Elm: A delightful language for reliable webapps
- Redux: A predictable state container for JavaScript apps.
- Composable Reducers
- 0069-composable-state-management-state-pullbacks
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
We now have a very basic version of our architecture in place. We have a store class that is generic over a state type which represents the full state of our application and its generic over an action type that represents all of the user actions that can take place in our application.
— 0:34
The store class wraps a state value, which is just a simple value type, and this allows us to once and for all hook into the observer so that we can notify SwiftUI anytime a change is about to happen to our state.
— 0:38
The store class also holds onto a reducer, which is the brains of our application. It describes how to take the current state of the application, and an incoming action from the user, and produce a whole new state of the application that can be then rendered and displayed to the user. Already this little bit of work as solved 2 of the 5 problems we outlined at the beginning of this episode.
— 0:46
But, as cool as all of this is, we can go further. Let’s address the problem that is starting to develop in our appReducer . Right now it’s looking pretty hefty: one giant reducer that is handling the mutations for 3 different screens. This doesn’t seem particularly scalable. If we had two dozen screens are we really going to want a single switch statement that switches over every single action of 24 different screens? That’s not going to work.
— 1:17
We need to investigate ways of composing reducers into bigger reducers. How can break up that one big reducer into lots of little tiny ones that do one specific thing and then glue them together to form our master appReducer ? Let’s start to study that. Combining reducers
— 1:41
Here’s our app reducer: func appReducer(value: inout AppState, action: AppAction) -> Void { switch action { case .counter(.decrTapped): state.count -= 1 case .counter(.incrTapped): state.count += 1 case .primeModal(.saveFavoritePrimeTapped): state.favoritePrimes.append(state.count) state.activityFeed.append( .init( timestamp: Date(), type: .addedFavoritePrime(state.count) ) ) case .primeModal(.removeFavoritePrimeTapped): state.favoritePrimes.removeAll(where: { $0 == state.count }) state.activityFeed.append( .init( timestamp: Date(), type: .removedFavoritePrime(state.count) ) ) case let .favoritePrimes(.deleteFavoritePrimes(indexSet)): for index in indexSet { let prime = state.favoritePrimes[index] state.favoritePrimes.remove(at: index) state.activityFeed.append( .init( timestamp: Date(), type: .removedFavoritePrime(prime) ) ) } } } It’s responsible for combining the current state of our entire application with any user action by performing the appropriate mutation to state. It’s handling three different screens and five different user actions. This function is already getting pretty long! As we get more and more screens, this function is going to get gigantic.
— 2:09
The beauty of expressing the fundamental unit of our architecture in a function is that it presents the possibility that we can compose them in interesting ways. Functions are infinitely composable, after all. And it turns out that the signature of a reducer function has a bunch of different types of composition, and they are exactly what we need to break our appReducer down into a bunch of smaller reducers that can be composed together.
— 2:36
Let’s start with the simplest kind of composition. What can you do if you have two reducers that operate on the same type of state and with the same type of actions? Is there a way to somehow combine them into a single reducer that does both reducers’ work at once? It is absolutely possible, and in fact it’s pretty easy to implement: func combine<Value, Action>( _ first: @escaping (inout Value, Action) -> Void, _ second: @escaping (inout Value, Action) -> Void ) -> (inout Value, Action) -> Void { return { value, action in first(&value, action) second(&value, action) } }
— 4:11
This simply says that to combine two reducers you can run the first on the state and then run the second.
— 4:34
And how can we use it? Well, let’s naively break up our big reducer into a few small reducers. It seems to me that there are really 3 reducers at play here: one for the counter screen, one for the modal and one for the favorite primes screen.
— 4:51
Let’s create a reducer for each one of those screens. Each will operate on the subset of actions that they care about: func counterReducer( value: inout AppState, action: AppAction ) -> Void { switch action { case .counter(.decrTapped): state.count -= 1 case .counter(.incrTapped): state.count += 1 default: break } } func primeModalReducer( state: inout AppState, action: AppAction ) -> Void { switch action { case .primeModal(.addFavoritePrime): state.favoritePrimes.append(state.count) state.activityFeed.append( .init( timestamp: Date(), type: .addedFavoritePrime(state.count) ) ) case .primeModal(.removeFavoritePrime): state.favoritePrimes.removeAll(where: { $0 == state.count }) state.activityFeed.append( .init( timestamp: Date(), type: .removedFavoritePrime(state.count) ) ) default: break } } func favoritePrimesReducer( state: inout AppState, action: AppAction ) -> Void { switch action { case let .favoritePrimes(.removeFavoritePrimes(indexSet)): for index in indexSet { state.activityFeed.append( .init( timestamp: Date(), type: .removedFavoritePrime(state.favoritePrimes[index]) ) ) state.favoritePrimes.remove(at: index) } default: break } }
— 6:04
However, we still need to glue them together into the master reducer that will run the logic for the entire app. We can now use our combine function to recreate an app reducer that behaves exactly as it did before: let appReducer = combine( combine(counterReducer, primeModalReducer), favoritePrimesReducer )
— 7:02
Because combine only works with two reducers at a time, it’s a little awkward to use with nesting. We’re going to want to combine many, many reducers across many screens in our applications, so this isn’t going to scale well.
— 7:21
We can fix this by allowing the combine function to work with any number of reducers by using variadics: func combine<Value, Action>( _ reducers: (inout Value, Action) -> Void... ) -> (inout Value, Action) -> Void { return { value, action in for reducer in reducers { reducer(&value, action) } } }
— 7:47
And now we can write our app reducer as simply: let appReducer = combine( counterReducer, primeModalReducer, favoritePrimesReducer )
— 8:00
This combines all of our smaller reducers into one big reducer, and our app still works exactly as it did before.
— 8:12
And this is our first flavor of composition when it comes to reducers. It’s the simplest, but already it has allowed us to break our big appReducer that was about 30 lines of code into 3 separate reducers, each of which is like 10 lines of code. Focusing a reducer’s state
— 8:37
Now although we have broken down our large appReducer into some smaller pieces, each of those pieces have some problems of their own which gives us some hints that they are yet more forms of composition to discover.
— 9:04
For example, consider the counter reducer: func counterReducer( state: inout AppState, action: AppAction ) -> Void { switch action { case .counter(.decrTapped): state.count -= 1 case .counter(.incrTapped): state.count += 1 default: break } }
— 9:11
This reducer wants to accomplish something quite simple, just incrementing and decrementing a single integer, yet it is taking in the full app state, which includes favorite primes, activity feeds and more. Really what we would like is to have this reducer work just on a simple integer like this: func counterReducer( // state: inout AppState, action: AppAction state: inout Int, action: AppAction ) -> Void { switch action { case .counter(.decrTapped): // state.count -= 1 state -= 1 case .counter(.incrTapped): // state.count += 1 state += 1 default: break } }
— 10:07
Even though the logic is the same and the number of lines of code is the same, this reducer is inherently simpler than the previous one simply because it operates on a smaller set of state. Someone who isn’t familiar with this code can look at the signature of this function and know that this reducer only deals with a simple integer and not the entirety of the AppState model.
— 10:25
However, changing this signature has broken something: let appReducer = combine( counterReducer, primeModalReducer, favoritePrimesReducer ) Cannot convert value of type ‘(inout Int, AppAction) -> ()’ to expected argument We can no longer combine all of these reducers together because they don’t all speak the same state. The counterReducer only wants to work on integers yet the other two reducers work on the full app state. What to do about this?
— 10:40
This brings us to our next form of composition on reducers: pullback!
— 10:46
Pullbacks are an operation we have come across twice on Point-Free before. First we uncovered a strange form of composition that arises when studying a topic known as “ contravariance .” This is a form of composition that reverses the direction of function arrows. An example of this is predicates, which are functions from some type to booleans, like (A) -> Bool . We saw that if we have a function from A to B then we can turn this into a function from predicates on B s to predicates on A s. Notice that the direction flipped: we started with A to B and we ended up going from predicate on B s to predicate on A s. This is in stark contrast with arrays, in which if you have a function from A to B you can turn that into a function from arrays of A s to arrays of B s. Notice that the function arrow direction was preserved.
— 11:32
We called this operation pullback because it was useful for transforming predicates on small, specific data into predicates on large, general data. For example, given a predicate on integers we could pull it back to be a predicate on user models by projecting into the user’s id field.
— 11:42
All of those discoveries were really cool and powerful, but then something amazing happened. More than 6 months after that episode on contravariance came out we encountered pullbacks again in a completely unrelated area: snapshot testing! We showed that a snapshot library we were designing had the concept of pullbacks. That is, if we had a function from A to B we could turn that into a function from snapshot strategies on B s to snapshot strategies on A s. Notice again the direction is reversed. This operation is what allowed us to derive more general snapshot strategies from specific snapshot strategies. For example, we could take a snapshot strategy on UIImage s and pull it back to work on CALayer s by just rendering the CALayer into a UIImage . We could then pullback that snapshot strategy on CALayer s to UIView s by just plucking out the layer inside a view. And finally we could pullback the snapshot strategy on UIView s to UIViewController s by just plucking out the view inside the controller. And with very little work we were able to get 3 all new snapshot strategies out of this simple pullback operation.
— 12:18
Now hopefully the way I’m describing this operation is starting to tickle something in the back of your mind, because it sounds like a pullback operation would be mighty useful for our reducers. For both predicates and snapshot strategies, the pullback was useful for taking something that worked on a small piece of data and transforming it into something that worked on a large piece of data, as long as we had a means of projecting from the large data into the small data. That is precisely what we want to do with reducers: we want to take a reducer on a small piece of substate and transform it into a reducer that works on global state, of which the substate embeds inside it. Pulling back reducers along state
— 12:55
Let’s explore how we might define a pullback on reducers. At its core we want a function that can transform a reducer on local state into one on global state, so we can start with this signature, even though it is currently incomplete: func pullback<LocalValue, GlobalValue, Action>( _ reducer: @escaping (inout LocalValue, Action) -> Void ) -> (inout GlobalValue, Action) -> Void { }
— 13:40
This is the core of what we want to accomplish. Now of course we can’t implement this function right now because the LocalValue and GlobalValue generics are in no way connected right now. We need some way to relate them before we can even dream of implementing this.
— 13:56
In our episodes on contravariance and on snapshot testing it was enough to relate these two generics with just a simple function: func pullback<LocalValue, GlobalValue, Action>( _ reducer: @escaping (inout LocalValue, Action) -> Void, _ f: @escaping (GlobalValue) -> LocalValue ) -> (inout GlobalValue, Action) -> Void { } That is, if you provided a way to go from the global value to the local value, you could transform a reducer on local values into one on global values.
— 14:20
However, this is not going to be sufficient for this pullback. To see why, let’s try implementing: func pullback<LocalValue, GlobalValue, Action>( _ reducer: @escaping (inout LocalValue, Action) -> Void, _ f: @escaping (GlobalValue) -> LocalValue ) -> (inout GlobalValue, Action) -> Void { return { globalValue, action in var localValue = f(globalValue) reducer(&localValue, action) } }
— 14:59
Although this has implemented the function, and the compiler seems to even be happy, it can’t possibly be correct. Notice that we are creating a local mutable copy of the local value, and then using the reducer to mutate, but then we don’t do anything with that copy of the local value. This means the global value is not changing at all, and so with every pass of this reducer nothing will actually change.
— 15:27
To demonstrate this, we can satisfy the compiler: pullback(counterReducer) { $0.count }, But now we’ve broken the counter screen: none of the counter actions are doing anything.
— 15:56
The piece we are missing is the ability to take that new local value and stick it back into the global value. That sounds like that in addition to our function which can get local values from global values, we need a function that can set local values inside global values.
— 16:11
Something like: func pullback<LocalValue, GlobalValue, Action>( _ reducer: @escaping (inout LocalValue, Action) -> Void, get: @escaping (GlobalValue) -> LocalValue, set: @escaping (inout GlobalValue, LocalValue) -> Void ) -> (inout GlobalValue, Action) -> Void { return { globalValue, action in var localValue = get(globalValue) reducer(&localValue, action) set(&globalValue, localValue) } }
— 16:42
Now we can introduce the setter component of our counter reducer pullback: pullback( counterReducer, get: { $0.count }, set: { $0.count = $1 } ), And everything works as it did before!
— 17:10
Luckily Swift has a feature that bundles this pair of getters and setters into a single concept that has some really nice ergonomics and compiler support: it’s called key paths! We can essentially replace both of these get and set arguments with a single writable key path: func pullback<LocalValue, GlobalValue, Action>( _ reducer: @escaping (inout LocalValue, Action) -> Void, value: WritableKeyPath<GlobalValue, LocalValue> ) -> (inout GlobalValue, Action) -> Void { return { globalValue, action in reducer(&globalValue[keyPath: value], action) } }
— 17:46
The implementation got quite a bit shorter, and we can simplify the pullback to: pullback(counterReducer, value: \.count) Key path pullbacks
— 18:11
Now let’s take a minute to reflect on what we have done. Is it fair to call this operation “pullback”? Previously our pullbacks were along functions. That is we can pullback a predicate along a function that goes from a big piece of data to a small piece of data, or we could pullback a snapshot strategy along a function that goes from a big data structure to a small structure. Here we are not even using a function, we are using a key path. Is that an okay thing to do?
— 18:42
Well, it absolutely is, and it’s something we will dedicate a future episode to discussing, but we were forced to confront it head-on today. The defining characteristic of the pullback operation is not necessarily that it specifically uses functions to do the pulling back along. But rather, it’s that we have some process that starts with A s and ends with B s. Now certainly functions fit the bill, they are in fact the de facto process we use to transform A s into B s, but there are other types of processes we can use, and key paths are one.
— 19:27
And this pullback still satisfies all the same properties that the other pullbacks we’ve encountered. For example, we’ve previously seen that if you do a pullback with the identity function you just get the thing you started with. Like pulling backing a snapshot strategy on strings along the identity function just gives you back the exact same snapshot strategy on strings. This also holds true for our new reducer pullback.
— 19:49
If you pullback a reducer along the identity key path, you will just get back the same reducer, which we can see doing the following: let _appReducer = combine( counterReducer, primeModalReducer, favoritePrimesReducer ) let appReducer = pullback(_appReducer, value: \.self)
— 20:06
Here \.self is the key path that goes from AppState to AppState by just doing the identity.
— 20:38
This allows us to generalize the concept of pullbacks beyond just functions, and we can now be comfortable with the idea of doing pullbacks with key paths. Pulling back more reducers
— 21:03
And now that we are comfortable with it, let’s try this trick on our other reducers.
— 21:11
We can start with the favorite primes reducer: func favoritePrimesReducer( value: inout AppState, action: AppAction ) -> Void { switch action { case let .favoritePrimes(.removeFavoritePrimes(indexSet)): for index in indexSet { state.activityFeed.append( .init( timestamp: Date(), type: .removedFavoritePrime(state.favoritePrimes[index]) ) ) state.favoritePrimes.remove(at: index) } default: break } }
— 21:15
This reducer is also operating on the full AppState model even though it looks like it only needs access to array of favorite primes and the activity feed. It doesn’t care about the current count value or the logged in user at all, so why should it have access?
— 21:34
Let’s refactor this by creating a new intermediate struct model that holds only the data we care about: struct FavoritePrimesState { var favoritePrimes: [Int] var activityFeed: [AppState.Activity] }
— 21:53
And we can update the reducer to use this struct instead. func favoritePrimesReducer( value: inout FavoritePrimesState, action: AppAction ) -> Void { switch action { case let .favoritePrimes(.removeFavoritePrimes(indexSet)): for index in indexSet { state.activityFeed.append( .init( timestamp: Date(), type: .removedFavoritePrime(state.favoritePrimes[index]) ) ) state.favoritePrimes.remove(at: index) } default: break } }
— 21:57
Nothing even needs to change in the body of the reducer. Now when someone comes to this reducer they can feel more comfortable that they are going to be able to understand it in its entirety because they can see it only has the capability of mutating a very small set of state in the app, just the favorite primes and activity feed.
— 22:08
Now that reducer is looking good, but we’ve yet again broken our app reducer: let appReducer = combine( pullback(counterReducer, value: \.count), primeModalReducer, favoritePrimesReducer ) Type of expression is ambiguous without more context
— 22:24
We need to fix this by pulling back favoritePrimesReducer so that it works on the full app state. This one will be a little different than the previous one because the state the favoritePrimesReducer does simply come from a single field on AppState but rather two fields.
— 22:40
Luckily, Swift will create key paths for computed properties, so we can introduce a computed property on AppState that does the work manually: extension AppState { var favoritePrimesState: FavoritePrimesState { get { return FavoritePrimesState( favoritePrimes: self.favoritePrimes, activityFeed: self.activityFeed ) } set { self.activityFeed = newValue.activityFeed self.favoritePrimes = newValue.favoritePrimes } } }
— 23:41
And with that custom get-set computed property we get a writable key path magically generated for us automatically by the compiler, and we can use it to pullback our reducer so that it fits in with the rest of our reducers: let appReducer = combine( pullback(counterReducer, value: \.count), primeModalReducer, pullback(favoritePrimesReducer, value: \.favoritePrimesState) )
— 23:58
Our app now compiles and everything runs just as it did, but now we have yet another reducer that is hyper focused on just the pieces of state it cares about.
— 24:14
There is this third reducer primeModalReducer , but it actually needs quite a bit of the app state. It needs the current counter, the favorite primes and the activity feed. This is a case in which maybe it is not necessary to focus this reducer since it needs so much state, and it can just stay as it is. Till next time
— 24:32
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.
— 24:48
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.
— 25:01
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.
— 25:11
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.
— 25:31
It sounds like we need to repeat the same story for actions that we have for state…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 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 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 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 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 Downloads Sample code 0069-composable-state-management-state-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 .