EP 71 · Composable State Management · Aug 26, 2019 ·Members

Video #71: Composable State Management: Higher-Order Reducers

smart_display

Loading stream…

Video #71: Composable State Management: Higher-Order Reducers

Episode: Video #71 Date: Aug 26, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep71-composable-state-management-higher-order-reducers

Episode thumbnail

Description

We will explore a form of reducer composition that will take our applications to the next level. Higher-order reducers will allow us to implement broad, cross-cutting functionality on top of our applications with very little work, and without littering our application code with unnecessary logic. And, we’ll finally answer “what’s the point?!”

Video

Cloudflare Stream video ID: 88f06efc98f435293b61d3109993e9d9 Local file: video_71_composable-state-management-higher-order-reducers.mp4 *(download with --video 71)*

References

Transcript

0:06

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.

1:18

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. What’s a higher-order reducer?

1:56

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? In fact, we’ve even already defined a couple!

2:07

combine is a higher-order reducer: it’s a function that takes a number of reducers as input (so long as they work with the same value and action types), and it returns a brand new reducer as output by running each of the given reducers. pullback is a higher-order reducer: it’s a function that takes a reducer as input and, given key paths that operate on local state and actions, returns a brand new reducer as output that operates on more global state and actions.

2:19

Given how powerful these two functions are, it’s reasonable to ask what other higher-order reducers are out there just waiting to be discovered? In order to explore this question, let’s write out the signature of a higher-order reducer just to explore what we have at our disposal. func higherOrderReducer( _ reducer: @escaping (inout AppState, AppAction) -> Void ) -> (inout AppState, AppAction) -> Void { } To keep things simple, this higher-order reducer is just acting on a reducer that operates on the full app state and app actions.

2:55

In general it is not necessary for a higher-order reducer to keep all of these types the same, it can definitely take in a reducer of one type and return one of a completely different type.

3:08

No matter what we need to return a function that takes in some state and actions, so we can start there: func higherOrderReducer( _ reducer: @escaping (inout AppState, AppAction) -> Void ) -> (inout AppState, AppAction) -> Void { return { state, action in } }

3:21

And then inside here there is no limit to what we can do. We could inspect the state and action before sending them on through to the reducer: func higherOrderReducer( _ reducer: @escaping (inout AppState, AppAction) -> Void ) -> (inout AppState, AppAction) -> Void { return { state, action in // do some computations with state and action reducer(&state, action) } }

3:40

Or we could send the state and action on through to the reducer and then inspect what came out the other side: func higherOrderReducer( _ reducer: @escaping (inout AppState, AppAction) -> Void ) -> (inout AppState, AppAction) -> Void { return { state, action in // do some computations with state and action reducer(&state, action) // inspect what happened to state? } }

3:49

We could filter which actions go through. We could even perform mutations to the state and actions before and after we invoke the reducer. We have a lot of power in this function, and its entirely because this is a higher-order reducer.

4:03

And it turns out that this higher-order reducer is going to be exactly what fixes a bug that we accidentally introduced into our app when we were building it with vanilla SwiftUI concepts. A few episodes ago, after having built the basic version of the app, we decided to add a new feature of an activity feed. It simply kept track of a timestamp and a few events that we were interested in, in particular when a prime was added or removed to our favorites.

4:44

We implemented this feature pretty naively, just making some small changes to our prime modal so that we could add these events to our state. Right now that logic sits in our reducer: 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) ) ) } }

5:21

However, the bug here is that this is not the only place where we mutate our list of favorite primes. There is this completely separate screen, the list of favorite primes, where you can also remove primes from your list of favorites. Initially we hadn’t thought to change our activity feed over there, and so we were missing out on some events.

5:43

The fix was easy enough, we just added that extra mutation logic in the view. Now that logic just lives in a reducer: 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) } } }

5:52

However, this still isn’t great. These two pieces of logic are closely related, yet are far apart in two different reducers. Those reducers may live in different files or even different modules, and it will be difficult to make sure to make changes in both places. What if we wanted to add new activity types? We would need to audit all of the actions in our reducers and make sure we are hooking into the right spots for adding the new events. Higher-order activity feeds

6:32

However, higher-order reducers can make this much better for us. Because they give us the ability to inspect the actions coming in at a high level, we can centralize all of our activity tracking logic into a single place.

6:47

Let’s try it out by updating this template of a higher-order reducer we have. We have to decide whether we want the passed in reducer to run before our activity feed logic or after. If it runs before then we would have already mutated the favorite primes array and so we won’t necessarily know what prime number was removed. So we want the reducer to run after our logic. Let’s start by switching on the action so that we can consider every possible app action that comes in: func higherOrderReducer( _ reducer: @escaping (inout AppState, AppAction) -> Void ) -> (inout AppState, AppAction) -> Void { return { state, action in switch action { case .counter(_): case .primeModal(_): case .favoritePrimes(_): }

7:41

There are many actions across screens to consider, but some of them are not important for the purpose of our activity feed. So we can just break inside those cases to ignore them: func higherOrderReducer( _ reducer: @escaping (inout AppState, AppAction) -> Void ) -> (inout AppState, AppAction) -> Void { return { state, action in switch action { case .counter: break case .primeModal(_): case .favoritePrimes(_): }

7:57

We can then expand the cases that we do care about. func higherOrderReducer( _ reducer: @escaping (inout AppState, AppAction) -> Void ) -> (inout AppState, AppAction) -> Void { return { state, action in switch action { case .counter: break case .primeModal(.removeFavoritePrimeTapped): case .primeModal(.saveFavoritePrimeTapped): case let .favoritePrimes(.deleteFavoritePrimes(indexSet)): }

8:21

And now we can add our activity feed logic in here, and rename the function accordingly: func activityFeed( _ reducer: @escaping (inout AppState, AppAction) -> Void ) -> (inout AppState, AppAction) -> Void { return { state, action in switch action { case .counter: break case .primeModal(.removeFavoritePrimeTapped): value.activityFeed.append( .init( timestamp: Date(), type: .removedFavoritePrime(value.count) ) ) case .primeModal(.addFavoritePrime): value.activityFeed.append( .init( timestamp: Date(), type: .saveFavoritePrimeTapped(value.count) ) ) case let .favoritePrimes(.deleteFavoritePrimes(indexSet)): for index in indexSet { value.activityFeed.append( .init( timestamp: Date(), type: .removedFavoritePrime(value.favoritePrimes[index]) ) ) } } reducer(&state, action) } }

10:26

Now how do we use this higher-order reducer? We just need to feed it our appReducer , which gives us a whole new reducer, and that will be the reducer that we use with our store. This means we need to change the creation of our store and the main content view like so: ContentView( store: Store( initialValue: AppState(), reducer: activityFeed(appReducer) ) )

11:04

And we can delete all of that activity feed logic from our other reducers, which makes them just that much more short and sweet: func counterReducer( state: inout Int, action: CounterAction ) -> Void { switch action { case .decrTapped: state -= 1 case .incrTapped: state += 1 } } func primeModalReducer( state: inout AppState, action: PrimeModalAction ) -> Void { switch action { case .addFavoritePrime: state.favoritePrimes.append(state.count) case .removeFavoritePrime: state.favoritePrimes.removeAll(where: { $0 == state.count }) } } func favoritePrimesReducer( state: inout FavoritePrimesState, action: FavoritePrimesAction ) -> Void { switch action { case let .removeFavoritePrimes(indexSet): for index in indexSet { state.favoritePrimes.remove(at: index) } } }

11:26

Extracting the activity feed logic also allows us to simplify further! Our favorite primes reducer no longer needs all of the state it’s being passed. struct FavoritePrimesState { var favoritePrimes: [Int] var activityFeed: [AppState.Activity] } It no longer needs the activity feed: it only cares about the favorite primes.

11:39

We can now get rid of the intermediate state we created. // struct FavoritePrimesState { // var favoritePrimes: [Int] // var activityFeed: [AppState.Activity] // }

11:45

Update the reducer to work with an array of integers. func favoritePrimesReducer( state: inout [Int], action: FavoritePrimesAction ) -> Void { switch action { case let .removeFavoritePrimes(indexSet): for index in indexSet { state.remove(at: index) } } } We have two compiler errors. One was an extension to app state to handle the favorite primes sub-state. extension AppState { var favoritePrimesState: FavoritePrimesState { Use of undeclared type ‘FavoritePrimesState’ We can comment out all that code. // extension AppState { // var favoritePrimesState: FavoritePrimesState { // get { // return FavoritePrimesState( // favoritePrimes: self.favoritePrimes, // activityFeed: self.activityFeed // ) // } // set { // self.activityFeed = newValue.activityFeed // self.favoritePrimes = newValue.favoritePrimes // } // } // }

12:03

And in our pullback, we can pluck out the app state’s favorite primes to pass it along to the favorite primes reducer.: pullback( favoritePrimesReducer, value: \.favoritePrimes, action: \.favoritePrimes )

12:15

The activityFeed higher-order reducer has even allowed us to simplify existing reducers by removing functionality from them that didn’t need to be there in the first place. Higher-order logging

12:25

This is now just 24 lines of very succinct application logic, and all the activity feed stuff has been moved out into the higher-order reducer. If we run the app everything is working just as before, but it’s hard to see that because we don’t have a screen for the activity feed. There’s no way to really confirm that the activity feed state mutations are happening exactly as they were before.

12:56

Well, we can address that quite easily by building a logger for our application. Wouldn’t it be great if we automatically logged every action that the user performed and the resulting state from that action. This could be great for debugging and inspecting what is going on in the application while we use it.

13:13

We might be tempted to add this logging to our store. We could just add a bit of logging directly in the send method: func send(_ action: Action) { self.reducer(&self.value, action) print("Action: \(action)") print("Value:") dump(self.value) print("---") }

13:45

However, this means anyone who uses this store class, which is something that could be put into a library and shared, will be forced to have logging in their application. They may not want logging, or they may even want to format their logs differently, or even send them to os_log instead of just doing simple print statements like we are doing here.

14:07

Luckily we don’t need to bake the concept of logging directly into our store. Instead, this feature can be implemented in user-land, outside the scope of the library, and we will do it using another higher-order reducer.

14:15

Logging can be a higher-order reducer in which we wrap an existing reducer and will do some printing after the reducer runs. Let’s do just that: func logging( _ reducer: @escaping (inout AppState, AppAction) -> Void ) -> (inout AppState, AppAction) -> Void { return { value, action in reducer(&value, action) print("Action: \(action)") print("State:") dump(value) print("---") } }

15:11

And with this higher-order reducer we can wrap our appReducer to get a brand new reducer that does logging. However, this logic is needlessly specific. We don’t use anything about AppState or AppAction in the body, so we should make it generic over any type of value and action: func logging<Value, Action>( _ reducer: @escaping (inout Value, Action) -> Void ) -> (inout Value, Action) -> Void { return { value, action in reducer(&value, action) print("Action: \(action)") print("State:") dump(value) print("---") } }

15:39

And because this is a function, we could even introduce additional configuration as inputs, like specifying how and where the logging takes place.

15:58

To use this new higher-order reducer we want to plug our appReducer into it. But our appReducer is already being wrapped in another higher-order reducer, so really we have to nest these: ContentView( store: Store( value: AppState(), reducer: logging(activityFeed(appReducer)) ) )

16:12

And now when we run the app we can see that everything behaves as before, but we now get some logging in the console. And we can finally guarantee that adding and removing favorite primes is appending to our activity feed like we expect.

17:00

The way we are composing these higher-order reducers still isn’t great. If we had a bunch of these, this composition nesting would get messy fast: bar(foo(logger(activityFeed(appReducer))))

17:18

This can be cleaned up if we use some of the functions we have made available in our library of function composition helpers, known as Overture. In this library there is a function called compose for composing functions together, and a function called with that is simply giving a name to function application. We have those helpers copied into our Sources directory, and so we can make use of them like so: // import Overture ContentView( store: Store( value: AppState(), reducer: with( appReducer, compose( logger, activityFeed ) ) ) )

18:07

This is nice and tidy. We see we have our main appReducer that takes care of all of our application logic, and then with it we apply the composition of all of our higher-order reducers, such as logging and the activity feed.

18:36

It’s worth taking a moment to realize how powerful this is. higher-order reducers have given us the ability to add sweeping functionality to our application with little work. This is sometimes known as “aspect oriented programming” or “cross-cutting concerns”, because we have been able to tap into many fundamental aspects of our application without littering our application with logic that it mostly doesn’t care about.

19:13

And this stuff just isn’t possible with vanilla SwiftUI. There is no way to get logging on every action that a user performs and log the state of your components after every mutation. And here we are basically getting it for free.

19:25

And this is only the beginning of higher-order reducers. They also unlock the ability for us to lift reducers up into a world where they can work on lists of items. They can also turn reducers that work on arrays of models into reducers that also know how to paginate. The sky is the limit when it comes to higher-order reducers. What’s the point? We have now covered many different aspects of “composable” state management.

20:15

We have shown how to distill the essence of application state management into the concept of a reducer, where we take the app’s current state and a user action, and combine them to obtain the new state of the app.

20:31

We then showed that reducers emit 4 types of composition:

20:37

Reducers that operate on the same type of state and action can be combined together to form a single reducer

20:51

Reducers that operate on a small piece of sub-state can be pulled back to work on global state

21:06

Reducers that operate on a small subset of actions can be pulled back to work on global actions.

21:27

And functions that take reducers as input and return reducers as output can be composed together to layer on lots of additional functionality.

21:48

And using all of those forms of composition we were able to break down our application logic into many small reducers, each of which is easy to understand, and piece them together.

22:33

This has all been cool, and I can definitely see how functional programming has been influential in designing this architecture, but we’ve gotta ask ourselves, “what’s the point?”. Is it worthwhile to adopt this architecture when it’s not something that Apple is giving us directly. Are we going too far against the grain by introducing this layer on top of SwiftUI?

23:02

We think it absolutely worth exploring this direction of an app architecture in SwiftUI. Although it is true that Apple is not directly blessing this architecture, it is also true that there are lots of problems left unsolved by Apple that are on us to solve. So no matter what, we must introduce some layer on top of SwiftUI to handle these problems, and the layer we have discovered in these episodes is surprisingly lightweight.

23:48

Let’s take a look at the parts that could be considered “library” code: class Store<Value, Action>: ObservableObject { let reducer: (inout Value, Action) -> Void @Published var value: Value init( initialValue: Value, reducer: @escaping (inout Value, Action) -> Void ) { self.value = value self.reducer = reducer } func send(_ action: Action) { self.reducer(&self.value, action) } } func combine<Value, Action>( _ reducers: (inout Value, Action) -> Void... ) -> (inout Value, Action) -> Void { return { value, action in for reducer in reducers { reducer(&value, action) } } } 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) } }

24:26

This is just 34 lines of code, including newlines and lines that are just syntax. The real meat of it is really only 14 lines or so.

24:36

So as an architectural layer there really isn’t that much to it. But, by embracing this thin layer we are starting to see a ton of benefits.

26:23

For one thing, our views have become incredibly simple. They no longer do any direct mutations to state. They are basically functions that simply transform some observable object into view hierarchy, and the parts where we need to tap into an action callback we simply turn that action into a concrete data type and send it off to the store. Previously we multiple lines of mutations happening in each of these action closures.

26:45

Further, it becomes super clear where mutations are happening in our application. In fact, all we have to do is search for .send( in this file and we will literally see every single state mutation. This is incredibly powerful, and for a newcomer to this codebase it can be empowering to know that there is a consistent place to search for mutations and a sanctioned way to perform mutations.

27:09

We can even kick this up a notch. We can force all mutations to go through the store so that it is absolutely impossible to mutate the app state in any other way. All we have to do is change the value property in the store to be private(set) : @Published private(set) var value: Value

27:36

Now it is literally impossible to mutate the app’s state without going through the send method. This means you can search for 100% of all mutations that happen in your application simply be searching for .send( . That is very powerful.

28:00

So at a very surface level, our chosen architecture has already given us a level of consistency that vanilla SwiftUI does not give us. But, whenever we adopt a layer on top of whatever Apple gives us we run the risk of painting ourselves into a corner. That is, we end up something that isn’t super flexible, and although we may have solved one problem we may have also opened ourselves up to lots of new problems!

28:32

However, we don’t feel that is the case here. We found that our architecture is super composable. In fact, it supports 4 types of composition, and each type empowers us to create reducers that focus in on just the bare essentials to get their job done while still leaving themselves open to be plugged into the greater app. This is possible due to our choice to model the architecture on just simple functions, which are the pinnacle of composability.

29:22

But perhaps the coolest part of this architecture, and one of the huge benefits, is that it bears a striking resemblance to many concepts we have covered on Point-Free in the past. When discussing things like view styling , randomness , snapshot testing , and parsing we had a strict focus on building the correct atomic primitive and the operations that allow us to build up complex objects from that atomic unit.

29:29

For example, for randomness we started with a single generator of UInt32 s and used map , zip and flatMap to build up a password generator and even a generator that created random works of art. Then for snapshot testing we were able to start with a single strategy for snapshotting UIImage s and then used pullback s to create strategies on CALayer , UIView and UIViewController . And then for parsing we again started with a few basic parsers, like parsers of literals, doubles and characters, and we used map , zip and flatMap to build up truly complex parsers, like a parser of marathon races from a string that held race names, entrance fees and route geo coordinates.

30:52

We think it is amazing that we can talk about application architecture in the same way we talked about randomness, snapshot testing and parsing. This shows just how universal these ideas are. We repeatedly employ the same techniques to attack problems and we repeatedly get benefits out of it, and stumble upon new applications of these techniques that would have been difficult to predict from the outset.

31:28

So, that concludes our series of episodes on composable state management. However, we are far from done with this architecture. First of all, we have only addressed maybe 2 and a half of the 5 problems we outlined at the beginning of this series.

31:49

We said we wanted a modular architecture, so that components could be isolated, and perhaps even put into their own module. We are halfway to that goal with our ability to break reducers down into smaller reducers, but the views themselves still depend on a store of the full app state and app actions. We need a way to break down that object, and we will be starting that topic next time.

32:18

We also said that we wanted to have a story for how our application handles side effects, and we haven’t even touched that topic. That will also be coming soon. And finally, we claimed that developing applications in this style of architecture would lead us to a more testable application. We have yet to touch on that, but rest assured it’s possible, and we will also be covering that soon!

32:28

Until 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 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 0071-composable-state-management-hor 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 .