EP 72 · Modular State Management · Sep 9, 2019 ·Members

Video #72: Modular State Management: Reducers

smart_display

Loading stream…

Video #72: Modular State Management: Reducers

Episode: Video #72 Date: Sep 9, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep72-modular-state-management-reducers

Episode thumbnail

Description

In exploring four forms of composition on reducer functions, we made the claim that it gave us the power to fully isolate app logic, making it simpler and easier to understand. This week we put our money where our mouth is and show just how modular these reducers are!

Video

Cloudflare Stream video ID: ba03fcf782aa97b7cab22e537ee8ae67 Local file: video_72_modular-state-management-reducers.mp4 *(download with --video 72)*

References

Transcript

0:06

For the past 4 weeks we have been exploring an application architecture that heavily takes inspiration from functional programming. It’s primary, atomic unit is just a function, known as a reducer, and we discovered many different ways these functions can be composed together, which is a story that has played out many times on Point-Free.

0:24

We started this exploration because we saw that although SwiftUI solves many problems that we face in building an application, and does so in a beautiful and powerful way, there are still some things it doesn’t solve. In particular, we need to know how to do things like:

0:37

Create complex app state models, ideally using simple value types.

0:42

Have a consistent way to mutate the app state instead of just littering our views with mutation code.

0:49

Have a way to break a large application down into small pieces that can be glued back together to form the whole.

0:57

Have a well-defined mechanism for executing side effects and feeding the results back into our application.

1:03

Have a story for testing our application with minimal setup and effort.

1:07

So far we have solved about two and half of these problems:

1:10

We now model state as a simple value type.

1:15

We now mutate our state in a consistent way.

1:23

And finally, we were able to break down a very large application-wide reducer into small, screen-specific reducers by using a variety of compositions.

1:31

However, we consider the last point to only be solving half of the modularity story when it comes to this architecture. Although our reducers can be split up and moduralized, the views that render the state and send actions to the store cannot. They still operate on the full universe of app state and app actions.

1:46

If we were able to focus the store on just the state and actions a particular view cares about, then we increase our chances that the view could be extracted out into its own module. This would be a huge win. The inability to isolate components of an application is perhaps one of the biggest sources of complexity we see in other people’s code bases. Components start to become unnecessarily entangled with each other, and it can be difficult to understand all the ways in which a component can change over time.

2:25

So today we are going to complete the story of modularizing our architecture by first directly showing what it means to moduralize and why it’s beneficial, and then by showing how our Store type supports two types of transformations that allow us to focus its intentions onto only the things the view truly cares about.

2:44

Let’s start with a quick tour of the code we’ve written so far. Recap

2:50

The app we’ve been building is a “favorite primes counting app.”

2:55

From the root view we can drill down into a counter, which displays a number that can be incremented and decremented. We can ask if the current number is prime, and if it is, we can save it to our favorites, or remove it from our favorites if we change our mind. We can also ask for the nth prime, which hands the request off to Wolfram Alpha, a powerful scientific computing platform.

3:23

If we go back to the root view we can also drill down into a list of all of the favorite primes that we’ve saved. We can even delete any primes that have fallen out of our favor.

3:35

It’s a bit of a toy example, but it has many of the hallmarks of more moderately complex, real-world applications. In particular:

3:42

It manages global, mutable state that persists across multiple screens

3:46

It executes a side effect in the form of a network request

3:49

In previous episodes, we were building everything in a playground, but in order to prepare for modularization, we have moved this code into a dedicated iOS project. We used the “Single View” app template configured to use SwiftUI, and made the following minimal changes:

4:03

We replaced the stubbed-out ContentView with the contents of our playground, excluding the top-level, playground-specific logic

4:19

We configured our root view in the project’s scene delegate with a store

4:27

And we brought in some utils from the playground’s sources directory, including some Wolfram Alpha API code

4:34

Let’s walk through the file that contains all of our application logic so far.

4:39

At the top we have the core library code that powers our app architecture, starting with the Store class. Store is a container for mutable app state and all of the logic that can mutate it. It also bridges our app state to SwiftUI by conforming to the ObservableObject protocol. final class Store<Value, Action>: ObservableObject { let reducer: (inout Value, Action) -> Void @Published private(set) var value: Value init( initialValue: Value, reducer: @escaping (inout Value, Action) -> Void ) { self.reducer = reducer self.value = initialValue } func send(_ action: Action) { self.reducer(&self.value, action) } }

4:54

Next, we have two functions that form the foundation of reducer composition, starting with the combine function, which allows us to join multiple reducers together into a single mega reducer: func combine<Value, Action>( _ reducers: (inout Value, Action) -> Void... ) -> (inout Value, Action) -> Void { return { value, action in for reducer in reducers { reducer(&value, action) } } }

5:05

And the pullback function, which lets us transform a reducer that understands local state and actions into one that understands global state and actions: func pullback< LocalValue, GlobalValue, LocalAction, GlobalAction >( _ 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) } }

5:13

This is the operation that allowed us to make a reducer focus only on the state and actions it cares about, instead of the full universe of state and actions.

5:22

We also defined “higher-order” reducers : functions that take reducers as input and produce reducers as output. This allowed us to implement app-level “cross-cutting” concerns, like logging, in a central way that doesn’t pollute our more local reducers. func logging<Value, Action>( _ reducer: @escaping (inout Value, Action) -> Void ) -> (inout Value, Action) -> Void { … }

5:39

Then we have AppState , a struct that models our entire app state in a simple value type: struct AppState { var count = 0 var favoritePrimes: [Int] = [] var loggedInUser: User? = nil var activityFeed: [Activity] = [] struct Activity { let timestamp: Date let type: ActivityType enum ActivityType { case addedFavoritePrime(Int) case removedFavoritePrime(Int) } } struct User { let id: Int let name: String let bio: String } }

5:45

And we have a bunch of enums that describe all of our app’s user actions over various components and screens. enum CounterAction { case decrTapped case incrTapped } enum PrimeModalAction { case saveFavoritePrimeTapped case removeFavoritePrimeTapped } enum FavoritePrimesAction { case deleteFavoritePrimes(IndexSet) }

5:51

Each one of these enums come together in a single AppAction enum. enum AppAction { case counter(CounterAction) case primeModal(PrimeModalAction) case favoritePrimes(FavoritePrimesAction) var counter: CounterAction? { … } var primeModal: PrimeModalAction? { … } var favoritePrimes: FavoritePrimesAction? { … } }

5:55

It’s worth noting that we are using a code generation tool that we developed in previous Point-Free episodes to generate what we call “ enum properties .” These are computed properties that bridge an ergonomic gap between structs and enums by providing dot-syntax access to an enum case’s associated value. With these properties defined, Swift will automatically synthesize key paths, which gives us that ability to pull reducers of local actions back to reducers of global actions.

6:24

Next, we have our reducers, which describe all of the business logic of our app, broken down into various components. Each is responsible for handling the state and actions for each of the three screens in our app: the counter screen, the prime modal, and the favorites list. func counterReducer(state: inout Int, action: CounterAction) { … } func primeModalReducer(state: inout AppState, action: PrimeModalAction) { … } func favoritePrimesReducer(state: inout [Int], action: FavoritePrimesAction) { … }

6:39

Before finally coming together in our mega appReducer , which is built by pulling back each of these smaller, more focused reducers and combining them. let appReducer: (inout AppState, AppAction) -> Void = combine( pullback(counterReducer, value: \.count, action: \.counter), pullback(primeModalReducer, value: \.self, action: \.primeModal), pullback( favoritePrimesReducer, value: \.favoritePrimes, action: \.favoritePrimes ) )

6:48

We defined one more higher-order reducer that was a bit more domain-specific: it was responsible for appending activities to our app’s activity feed upon receiving certain actions. func activityFeed( _ reducer: @escaping (inout AppState, AppAction) -> Void ) -> (inout AppState, AppAction) -> Void { … }

6:59

Finally, we have all of our views, which are incredibly simple in this architecture because we’ve extracted all of our app logic into a reducer held by a Store . Views now merely describe a hierarchy of subviews given a store’s current value, and feed user actions back into the store. struct CounterView: View { … } struct IsPrimeModalView: View { … } struct FavoritePrimesView: View { … } struct ContentView: View { … } What does modularity mean?

7:18

We are now ready to start modularizing our application, but before we begin, we need to define what we mean by “modularity.”

7:25

In this case we mean, literally, “modules.” Modules are self-contained units of code that can be imported and used in your application. This includes modules that ship with Swift, like Foundation and Dispatch, platform-specific modules like Combine and SwiftUI, and even third-party libraries that you may have introduced into your code base.

7:42

Modules are a public interface to all sorts of functionality and behavior that they choose to expose. But most importantly, modules get zero access to anything the importer is doing: they can’t know about your types, your view controllers, and so on, and this is the power: modules are isolated from code that depends on them.

8:03

We think it’s important to take things further by breaking your own application down into modules. By doing so, you create easier-to-understand units that can be built, tested, and distributed in isolation. Then, your application can import all of these units and compose them together.

8:31

So how are we going to break our app up into these smaller units? There are a number of ways to build modules for Swift. We have frameworks, static libraries, and even Swift packages. The Swift Package Manager has been around since the first open source release of Swift 2.2 in 2015. However, it was only recently integrated into the Xcode 11 beta. We’re super excited about SwiftPM, but it’s still a lil early days and doesn’t yet have support for some features that we need in UI development, like the ability to hold resources, like images, so today we’ll be modularizing with frameworks instead. Modularizing our reducers

9:17

We’ve made the claim that our reducers are modular, so it’s time to put that to the test: let’s extract them into some first-class modules.

9:22

Every reducer we’ve defined is a self-contained unit describing a component’s logic, which means we should be able to extract each reducer into a module of its own. We have three reducers, counterReducer , primeModalReducer , and favoritePrimesReducer , and each of them represents mutations that can be made to the counter screen, prime modal, and favorite primes screen accordingly.

9:37

Let’s go to our project file and add a framework target for each reducer. We can create a “Counter” framework, a “PrimeModal” framework, and a “FavoritePrimes” framework.

10:23

We also have that core library code that powers our architecture, including the Store class and the reducer-composing functions, so let’s create one more framework called “ComposableArchitecture.” Modularizing the Composable Architecture

10:53

We can begin to extract this library code by adding a single source file to the “ComposableArchiteture” framework called “ComposableArchitecture.swift.”

11:11

Then we can cut and paste our library code into it: final class Store<Value, Action>: ObservableObject { … } func combine<Value, Action>( _ reducers: (inout Value, Action) -> Void... ) -> (inout Value, Action) -> Void { … } func pullback<LocalValue, GlobalValue, LocalAction, GlobalAction>( _ reducer: @escaping (inout LocalValue, LocalAction) -> Void, value: WritableKeyPath<GlobalValue, LocalValue>, action: WritableKeyPath<GlobalAction, LocalAction?> ) -> (inout GlobalValue, GlobalAction) -> Void { … } public func logging<Value, Action>( _ reducer: @escaping (inout Value, Action) -> Void ) -> (inout Value, Action) -> Void { … }

11:40

Because this code is self-contained, the module can already be built.

11:59

It can’t, however, be used, because none of its APIs have been made public. By default, every Swift interface (types, properties, methods, functions, and so on) has “internal” visibility. This means that every source file in the same target can access it by default, but it will be hidden to external modules. An interface must be prefixed with the public modifier to be accessible when its module is imported.

12:22

We want access to Store in our application, so it should be public. public final class Store<Value, Action>: ObservableObject {

12:24

But we don’t need its reducer to be public. In fact, we can probably make it private because it’s not accessed outside of the class. private let reducer: (inout Value, Action) -> Void

12:31

Its value should be accessible to our application’s views, so we’ll make it public and leave its setter private. @Published public private(set) var value: Value

12:44

Its initializer and send method should be public so that our application has the ability to create stores and send them user actions. public init( initialValue: Value, reducer: @escaping (inout Value, Action) -> Void ) { … public func send(_ action: Action) {

12:50

And finally, the combine , pullback , and logging functions should be public so that our application can compose a root reducer out of smaller parts. public func combine<Value, Action>( _ reducers: (inout Value, Action) -> Void... ) -> (inout Value, Action) -> Void { … public func pullback< LocalValue, GlobalValue, LocalAction, GlobalAction >( _ reducer: @escaping (inout LocalValue, LocalAction) -> Void, value: WritableKeyPath<GlobalValue, LocalValue>, action: WritableKeyPath<GlobalAction, LocalAction?> ) -> (inout GlobalValue, GlobalAction) -> Void { … public func logging<Value, Action>( _ reducer: @escaping (inout Value, Action) -> Void ) -> (inout Value, Action) -> Void {

12:58

After extracting the code and auditing its interface, we can finally import the module into our app. We’ll need to do so in “ContentView.swift”: import ComposableArchitecture

13:08

And in “SceneDelegate.swift”: import ComposableArchitecture

13:14

Everything builds and runs, and because no code was changed, it works just as before.

13:24

We should note that the ComposableArchitecture is very generic, reusable code, and we could even extract it from the current app into its own repo so that we can use it in many different applications. Maybe we could even open source it some day 😉. Modularizing the favorite primes reducer

13:41

Let’s get back to more app-level modularization. We can start with something simple: the favorite primes reducer.

13:50

First, we need to introduce a source file to the “FavoritePrimes” module. We can call it “FavoritePrimes.swift.” And then we can move the favoritePrimesReducer into it. func favoritePrimesReducer( state: inout [Int], action: FavoritePrimesAction ) { switch action { case let .deleteFavoritePrimes(indexSet): for index in indexSet { state.remove(at: index) } } }

14:29

This reducer depends on the FavoritePrimesAction enum, so we’ll want to move that too. enum FavoritePrimesAction { case deleteFavoritePrimes(IndexSet) }

14:40

And like that, the “FavoritePrimes” module can already be built, but the app fails to build because it no longer has access to the favorite primes action or reducer.

15:01

We can import the module in “ContentView.swift”: import FavoritePrimes But we also need to mark things public in the favorite primes module so that they can be accessed when imported. public enum FavoritePrimesAction { … public func favoritePrimesReducer( state: inout [Int], action: FavoritePrimesAction ) {

15:18

Everything builds just fine.

15:28

That went by really fast, so let’s take another look at “FavoritePrimes.swift”: public enum FavoritePrimesAction { case deleteFavoritePrimes(IndexSet) } public func favoritePrimesReducer( state: inout [Int], action: FavoritePrimesAction ) { switch action { case let .deleteFavoritePrimes(indexSet): for index in indexSet { state.remove(at: index) } } }

15:30

We were able to extract these lines into a module and reincorporate them into our app so quickly and with so little work that it almost feels like we haven’t done anything at all: we just shuffled code along to another file. But because this file lives in its own module, we’ve actually done a lot!

15:46

Here we have eleven lines of hyper-focused, easy-to-read code. This code has no access to global app state or global user actions—it couldn’t use AppState or AppAction if it tried. Nor does it have access to anything but its contents and the Swift standard library. We’ve completely isolated it from the rest of our application and made it impossible for any of that code to spill over. Modularizing the counter reducer

16:13

One reducer down, two to go. The counter reducer is very simple, so let’s quickly power through it. public enum CounterAction { case decrTapped case incrTapped } public func counterReducer(state: inout Int, action: CounterAction) { switch action { case .decrTapped: state -= 1 case .incrTapped: state += 1 } }

16:50

Just a couple of cut and pastes, and a couple of public s and we can now: import Counter

16:57

Everything builds, runs, and another part of our app has been completely isolated from everything else. Modularizing the prime modal reducer

17:05

We have one more reducer to extract: the prime modal reducer. It’s a bit more complicated, but let’s take the same approach we did with our other reducers and see where things go wrong.

17:14

First, we’ll add a “PrimeModal.swift” source file to the “PrimeModal” framework, which will contain the primeModalReducer and its associated actions. public enum PrimeModalAction { case saveFavoritePrimeTapped case removeFavoritePrimeTapped } public func primeModalReducer(state: inout AppState, action: PrimeModalAction) { switch action { case .removeFavoritePrimeTapped: state.favoritePrimes.removeAll(where: { $0 == state.count }) case .saveFavoritePrimeTapped: state.favoritePrimes.append(state.count) } } Use of undeclared type ‘AppState’

17:44

And we have a problem: the primeModalReducer relies on AppState , but we definitely don’t want to bring all of AppState into the “PrimeModal” module, especially when the reducer only relies on a small subset of that state. In this case, it only needs the current count and the array of favorite primes.

18:02

One thing we can do is introduce a brand new type, PrimeModalState , that captures just the state that the prime modal reducer cares about. public struct PrimeModalState { public var count: Int public var favoritePrimes: [Int] }

18:29

Then we can update the prime modal reducer’s signature. func primeModalReducer( state: inout PrimeModalState, action: PrimeModalAction ) {

18:34

And because the fields are the same, none of the body even needs to change. The “PrimeModal” framework now builds on its own.

18:40

We should now be able to import things into “ContentView.swift” and see what happens. import PrimeModal

18:50

We only have a single error where we pass primeModalReducer to the pullback function: pullback(primeModalReducer, value: \.self, action: \.primeModal), Type of expression is ambiguous without more context

18:53

The error message isn’t great, but the problem is that primeModalReducer used to work with AppState , and now it works with PrimeModalState . Previously, we could use the identity key path, \.self , to leave AppState unchanged when pulling the reducer back to work with prime modal actions. But how can we pull PrimeModalState back to AppState ? We need a key path that goes from AppState to PrimeModalState , but no such key path exists.

19:12

We might be tempted to change things up and have AppState hold onto a top-level primeModal instead, which embeds count and favoritePrimes . struct AppState { // var count = 0 // var favoritePrimes: [Int] = [] var primeModal: PrimeModalState

19:29

But then we would be forced to access count and favoritePrimes by drilling down through the primeModal property. Both of these properties are accessed on screens other than the prime modal: the current count is displayed all over the counter screen, and the favorite primes are listed on the favorite primes screen. So this just doesn’t feel like the way to model things.

19:52

Instead, we can add a computed property to AppState that is responsible for taking these two bits of state and packaging them up as PrimeModalState . extension AppState { var primeModal: PrimeModalState { PrimeModalState.init }

20:28

We can’t instantiate a value because we don’t have access to that struct initializer that Swift conveniently generates for us. Those default, “member-wise” initializers are not public, so PrimeModalState can only currently be initialized from inside its own module.

20:39

Instead, we must define a public initializer. It’s simple enough: public struct PrimeModalState { public var count: Int public var favoritePrimes: [Int] public init(count: Int, favoritePrimes: [Int]) { self.count = count self.favoritePrimes = favoritePrimes } }

21:03

This is a bit of extra work that we need to do when modularizing code like this. We think the boilerplate is worth the price, though, and while we had to manually write things, a future version of Xcode should be able to do the work for us.

21:17

We can now construct one of these values: extension AppState { var primeModal: PrimeModalState { PrimeModalState( count: self.count, favoritePrimes: self.favoritePrimes ) }

21:27

But we’re not quite done yet. The pullback function requires a writable key path for mutations to work. We can make this property writable with a set block, which merely sets the two properties given a new prime modal state. extension AppState { var primeModal: PrimeModalState { get { PrimeModalState( count: self.count, favoritePrimes: self.favoritePrimes ) } set { self.count = newValue.count self.favoritePrimes = newValue.favoritePrimes } }

21:56

And we can finally replace the identity key path with a key path into prime modal state: pullback( primeModalReducer, value: \.primeModal, action: \.primeModal ),

22:18

Everything is building and working as before.

22:24

We’ve now fully modularized all of our app’s reducers. Two out of the three were very simple to extract: we merely had to move, publicize, and import some code. Modularizing the prime modal reducer wasn’t quite so straightforward because it had to access several, unbundled parts of app state. We found, though, that we have the ability to scope access by introducing an intermediate struct that holds just the state it cares about and nothing more. We can then introduce a computed property on global state into this more local state, which gives us a key path that we can pass to pullback when transforming a reducer on local state into a reducer on global state.

22:54

This is a handy trick that we can use, again and again, whenever we need to pass multiple bits of global state to a more local component. It does, unfortunately, come at the cost of boilerplate: we created a brand new type to represent this state, we defined a public initializer so that other modules can instantiate it, and we defined a computed property in order to use pullback .

23:25

It is possible to eliminate the initializer boilerplate by using a type alias of a tuple: public typealias PrimeModalState = ( count: Int, favoritePrimes: [Int] )

23:49

Everything still builds, and that is pretty nice. However, Swift has a weird relationship with tuples, and they often cause trouble when you use them too much. So, it’s up to you and your team if you would rather use tuples or structs. It may even make sense to start with tuples at the beginning and then only move to a struct if you need the extra structure.

25:07

The rest of the boilerplate is unavoidable, but it is pretty mechanical and easy to write, and we think it is incredibly worth it when the end result is another isolated, easy-to-reason-about module.

25:24

The ease at which we modularized these reducers speaks to just how modular this architecture is by default. We didn’t have to make any striking refactors or changes to logic because the boundaries are already very clearly defined. The worst things got was the need to introduce a little extra boilerplate for a component that had more complex state. Till next time…

26:03

While our architecture has seriously simplified our views, it still doesn’t have a story for modularizing them. We have isolated our reducers from the entirety of app state and app actions, but we have not yet isolated our views. So let’s start chipping away at that part of the problem…next time! References Why Functional Programming Matters John Hughes • Apr 1, 1989 A classic paper exploring what makes functional programming special. It focuses on two positive aspects that set it apart from the rest: laziness and modularity. https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf Package Resources Anders Bertelrud • Dec 7, 2018 A Swift Evolution proposal for the Swift Package Manager to support resources. https://github.com/abertelrud/swift-evolution/blob/package-manager-resources/proposals/NNNN-package-manager-resources.md Access Control Apple This chapter of the Swift Programming Language book explains access control in depth and how it affects module imports. https://docs.swift.org/swift-book/LanguageGuide/AccessControl.html 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 0072-modular-state-management-reducers 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 .