Video #202: Reducer Protocol: The Solution
Episode: Video #202 Date: Aug 29, 2022 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep202-reducer-protocol-the-solution

Description
Let’s begin to solve a number of the problems with the Composable Architecture by introducing a reducer protocol. We will write some common conformances and operators in the new style, and even refactor a complex demo application.
Video
Cloudflare Stream video ID: f11ab7550b050d98b25c4f019c30522a Local file: video_202_reducer-protocol-the-solution.mp4 *(download with --video 202)*
Transcript
— 0:05
So, we have now seen there is still a ton of room for improvement in the library:
— 0:09
We can do a better job of providing a more natural space for housing the state, actions and logic of your features built in the Composable Architecture.
— 0:17
We can help out the compiler a bit so that it is not so strained, leading us to lose type inference, autocomplete and warnings.
— 0:26
There’s improvements we can make to readability of highly composed reducers, as well as the correctness of some of the more powerful operators in the library.
— 0:35
We definitely have to do something about the ergonomics of the environment, because right now it’s quite a pain to add new dependencies to a leaf node of an application and update every layer through to the root of the application.
— 0:49
And finally, there’s performance improvements we can make because highly modularized applications will lead to very deep call stacks.
— 0:56
Well, luckily for us it’s possible to solve all of these problems, and more. By putting a protocol in front of reducers, and by constructing reducers as concrete types that conform to the protocol rather than deeply nested escaping closures, we will greatly improve the experience of developing large, complex features in the library. The protocol
— 1:16
So, let’s get to it.
— 1:19
Let’s create a new file called ReducerProtocol.swift .
— 1:36
The simplest way to translate the concrete Reducer struct into a protocol is by converting each generic to an associated type, and then one requirement for mutating the current state of the application given an action, and then returning an effect that can feed actions back into the system: public protocol ReducerProtocol { associatedtype State associatedtype Action associatedtype Environment func reduce( into state: inout State, action: Action, environment: Environment ) -> Effect<Action, Never> }
— 2:15
Sometime in the future we will actually rename this protocol to just Reducer , and then to construct a reducer from a closure like we currently a different type would be used, such as Reduce : Reduce { state, action, environment in … }
— 2:29
But we will wait awhile to do that so that we don’t flood everyone’s projects with deprecation warnings or, worse, compiler errors.
— 2:39
With this protocol defined we can create reducers by creating whole new types that conform to the protocol.
— 2:45
Now the library actually only ships with one single concrete reducer. That’s because there aren’t really any useful reducers the library could give all users of the library. Reducers tend to be very domain specific. The library does ship a lot of reducer operators, that is, functions that transform existing reducers into new, enhanced ones, and we will be looking at that in a moment.
— 3:07
The one single reducer the library ships with today is called the “empty” reducer because it represents a reducer that does nothing: extension Reducer { public static var empty: Reducer { Self { _, _, _ in .none } } }
— 3:20
This can be handy for plugging in a reducer where one is required, like in a function argument, before you have actually implemented the reducer.
— 3:28
We can define the equivalent with the ReducerProtocol like so: public struct EmptyReducer<State, Action, Environment>: ReducerProtocol { public init() {} public func reduce( into _: inout State, action _: Action, environment _: Environment ) -> Effect<Action, Never> { .none } }
— 3:55
A little more verbose, but it’s not so bad. The generics of the type fulfill the associated type requirements and so we don’t have to define explicit type aliases, which is nice.
— 4:13
We can also define more domain-specific reducers. Like say a counter reducer: struct Counter: ReducerProtocol { }
— 4:24
Now already we can see something cool. We can define the domain of this feature directly in the Counter struct. No need to create an empty enum for name spacing as the reducer conformance makes a natural name space. And better, defining the types inside will automatically satisfy the associated type requirements: struct Counter: ReducerProtocol { struct State { var count = 0 } enum Action { case incrementButtonTapped case decrementButtonTapped } struct Environment {} }
— 4:45
All that’s left is to implement the reduce method requirement: struct Counter: ReducerProtocol { … func reduce( into state: inout State, action: Action, environment: Environment ) -> Effect<Action, Never> { switch action { case .incrementButtonTapped: state.count += 1 return .none case .decrementButtonTapped: state.count -= 1 return .none } } }
— 4:57
And that’s it. It’s a little more verbose than defining a reducer with a closure, but we are no longer creating a file-scope variable to represent the reducer, and we are getting a natural place to stuff counter-related things.
— 5:27
We are also already getting compiler diagnostics. If we open up an Effect.run closure to perform some effectful work, we’ll see that autocomplete is working: return .run { send in send(.<#⎋#> } incrementButtonTapped decrementButtonTapped return .run { send in await send(.decrementButtonTapped) }
— 5:53
And if we introduce an unused variable: return .run { send in let x = 1 } Initialization of immutable value ‘x’ was never used; consider replacing with assignment to ‘_’ or removing it
— 5:55
We get a warning.
— 5:58
All of this is happening because we are no longer in one big closure defined at the file-scope of the module.
— 6:04
With our first concrete, domain-specific reducer defined we can now explore something that simply would not have been easily possible in the old-style of reducers defined as closures.
— 6:15
Suppose the counter feature had some dependencies. Just for simplicity, say it needs the current date and a scheduler to do its job: struct Counter: ReducerProtocol { … struct Environment { var date: () -> Date var mainQueue: AnySchedulerOf<DispatchQueue> } … }
— 6:36
The environment is a static thing throughout the lifetime of the application. It’s configured once at the root of the application and then sliced up into subsets of dependencies to pass down to child features. So, passing the same, static environment to the reduce method every time an action is sent into the system is equivalent to the Counter type just holding onto the environment and removing the environment from the reducer signature entirely: struct Counter: ReducerProtocol { … struct Environment { var date: () -> Date var mainQueue: AnySchedulerOf<DispatchQueue> } var environment: Environment func reduce( into state: inout State, action: Action ) -> Effect<Action, Never> { … } }
— 7:08
Then, at the moment of constructing the Counter reducer you are responsible for providing it all of its dependencies so that it can do its job.
— 7:14
In fact, at this point, why even define an environment struct at all when we can just inline the properties directly in the Counter type: struct Counter: ReducerProtocol { … var date: () -> Date var mainQueue: AnySchedulerOf<DispatchQueue> func reduce( into state: inout State, action: Action ) -> Effect<Action, Never> { … } }
— 7:30
We can even give these some reasonable defaults so that constructing Counter reducer instances is simple. var date: () -> Date = { Date() } var mainQueue: AnySchedulerOf<DispatchQueue> = .main
— 7:38
…but we will have an even better way of providing default dependencies soon.
— 7:42
This was not possible to do in the style of the Reducer struct because we didn’t have any place to store the dependencies. One thing we could have done is expressed all of our reducers as functions that take dependencies and then return a reducer: func counterReducer( date: () -> Date, mainQueue: AnySchedulerOf<DispatchQueue> ) -> Reducer<CounterState, CounterAction> { .init { state, action in … } }
— 8:20
But this would be really awkward to manage.
— 8:23
So, if it really does pan out to remove environments from the reducer protocol, it will be huge. By eliminating a whole associated type from the protocol we will massively simplify the cognitive overhead it takes to understand what a reducer is and how to implement it. Writing generic algorithms over the shape of reducers will also become simpler.
— 8:45
So, let’s move forward with this style and hope it all somehow works out for us in the end. We will update the ReducerProtocol to drop the environment associated type, and instead any reducers that want an environment will just need to add fields to its conforming type: public protocol ReducerProtocol { associatedtype State associatedtype Action func reduce( into state: inout State, action: Action ) -> Effect<Action, Never> }
— 8:54
This causes the EmptyReducer to no longer compile, but all we have to do is remove any mention of the environment: public struct EmptyReducer<State, Action>: ReducerProtocol { public init() {} public func reduce( into _: inout State, action _: Action ) -> Effect<Action, Never> { .none } } Operator conformances
— 9:04
Next let’s define a reducer operator. This is a way of transforming existing reducers into all new ones. Perhaps the simplest operator in the library is the “combine” function, which allows you to combine multiple reducers that all operate on the same domain into one big reducer.
— 9:28
There are 3 ways of combining in the library, but two of them defer to the third one: extension Reducer { public static func combine(_ reducers: [Self]) -> Self { Self { value, action, environment in .merge( reducers .map { $0.reducer(&value, action, environment) } ) } } }
— 9:34
This function takes an array of reducers and turns it into a single reducer by simply running each reducer, one after the other, and then merging all of their effects together into one big effect.
— 9:44
This allows you to combine a bunch of reducers at once: Reducer.combine([ reducerA, reducerB, reducerC ])
— 9:57
An overload of combine is provided to allow combining a variadic list of reducers rather than just an array: public static func combine( _ reducers: Self... ) -> Self { .combine(reducers) }
— 10:05
…so now you can do things like: Reducer.combine( reducerA, reducerB, reducerC )
— 10:10
And then another function, this time a method, that combine’s self with some other reducer: public func combined(with other: Self) -> Self { .combine(self, other) }
— 10:20
…which allows you to chain together a bunch of reducers if you prefer that style: reducerA .combine(with: reducerB) .combine(with: reducerC)
— 10:32
Let’s see what this looks like in the world of ReducerProtocol . We will start with this last variation because it’s in some sense simpler since it only deals with two reducers, and we will look at the other variations later.
— 10:44
To create a “reducer operator” in the protocol style we define a method on the actual ReducerProtocol that describes how we want the call site to look like. In particular we want a method that takes any other reducer that operates on the same domain: extension ReducerProtocol { public func combine<R: ReducerProtocol>( with other: R ) -> <#???#> where R.State == State, R.Action == Action { } }
— 11:44
This looks a verbose with all the generic constrains, though at least we only have 2 generics to constrain rather than 3. But, we will see later that we can massively simplify this signature using some new features from Swift 5.7.
— 11:58
We don’t yet know what we are going to return from this method. It needs to be another conformance to ReducerProtocol that somehow encapsulates the logic for running both reducers and merging their effects.
— 12:13
To do this we have to define a whole new type that conforms to the protocol: public struct CombineReducer: ReducerProtocol { }
— 12:23
And further it will be generic over the two types of reducers we are trying to combine: public struct CombineReducer<LHS, RHS>: ReducerProtocol { }
— 12:30
And those generics will need to conform to the reducer protocol, and further their state and action associated types will need to be equal since that’s the only way in which it makes sense to combine two reducers: public struct CombineReducer< LHS: ReducerProtocol, RHS: ReducerProtocol >: ReducerProtocol where LHS.State == RHS.State, LHS.Action == RHS.Action { }
— 12:44
Then the CombineReducer will hold onto those two reducers so that it can make use of them later: let lhs: LHS let rhs: RHS
— 12:57
And then we can actually implement the reduce requirement by running both reducers and merging their effects: public func reduce( into state: inout LHS.State, action: LHS.Action ) -> Effect<LHS.Action, Never> { .merge( self.lhs.reduce(into: &state, action: action), self.rhs.reduce(into: &state, action: action) ) }
— 13:31
This completes the implementation of CombineReducer , and this is now the type we want to return from the combine(with:) method: public func combine<R: ReducerProtocol>( with other: R ) -> CombineReducer<Self, R> where State == R.State, Action == R.Action { .init(lhs: self, rhs: other) }
— 13:47
And that finishes the implementation!
— 13:50
It’s definitely a lot more work than it was with the Reducer struct type, but it’s also not so bad, and we will see there are a ton of benefits to doing this way. It’s also worth noting that this style of API design is how a lot of super generic libraries are designed.
— 14:05
For example, our parser library is also defined as a protocol with a bunch of conformances for each type of parser and parser operator. The same is true of the Combine framework, the standard library’s Collection and AsyncSequence APIs, as well as Apple’s new regex library, and even SwiftUI’s view API could be seen like this.
— 14:25
In all of those libraries, the act of building up a large, complex value results in a large, deeply nested type which statically describes all the pieces that went into making it. And we are going to see that play out yet again with our reducers.
— 14:39
For example, if we apply the combine(with:) operator a few times we will see we get quite a big type: let reducer = Counter() .combine(with: Counter()) .combine(with: Counter()) .combine(with: Counter()) CombineReducer<CombineReducer<CombineReducer<Counter, Counter>, Counter>, Counter>
— 14:51
The more operators and types we use the more complicated and deeply nested this type will become.
— 15:00
There’s another fundamental operator the library comes with, and that’s pullback .
— 15:13
Currently this operator is defined as a method on the Reducer type: public func pullback< ParentState, ParentAction, ParentEnvironment >( state toLocalState: WritableKeyPath<ParentState, State>, action toLocalAction: CasePath<ParentAction, Action>, environment toLocalEnvironment: @escaping (ParentEnvironment) -> Environment ) -> Reducer< ParentState, ParentAction, ParentEnvironment > { .init { parentState, parentAction, parentEnvironment in guard let localAction = toLocalAction .extract(from: parentAction) else { return .none } return self.reducer( &parentState[keyPath: toLocalState], localAction, toLocalEnvironment(parentEnvironment) ) .map(toLocalAction.embed) } }
— 15:16
It’s a big signature, but its purpose is to transform a reducer that works on a child domain into a reducer that works on a parent domain, and it does so by supplying three transformations for turning the parent state, action and environment into a child state, action and environment.
— 15:46
We saw this operator in use last episode when we showed how a tab-based application could be split into 3 domains that are glued together: let appReducer = Reducer< AppState, AppAction, AppEnvironment >.combine( tabAReducer.pullback( state: \.tabA, action: /AppAction.tabA, environment: { _ in .init() } ), tabBReducer.pullback( state: \.tabB, action: /AppAction.tabB, environment: { _ in .init() } ), tabCReducer.pullback( state: \.tabC, action: /AppAction.tabC, environment: { _ in .init() } ) )
— 16:06
Let’s see what it takes to define the operator on the ReducerProtocol . Hopefully it’s a little simpler since we are no longer dealing with environments.
— 16:14
We can start by getting a stub of a method in place that takes the necessary arguments for transforming global domain into local domain, and then it has to return some kind of type that we haven’t yet defined: extension ReducerProtocol { public func pullback<ParentState, ParentAction>( state toChildState: WritableKeyPath<ParentState, State>, action toChildAction: CasePath<ParentAction, Action> ) -> <#???#> { } }
— 17:10
The type we return will be a brand new struct that conforms to the ReducerProtocol , and it needs to be generic over the parent domain we are transforming into, as well as the child reducer we are transforming: public struct PullbackReducer< ParentState, ParentAction, Child: ReducerProtocol >: ReducerProtocol { }
— 17:42
Inside this type we need to hold onto all the data it needs to do its job, and make an initializer: public struct PullbackReducer< ParentState, ParentAction, Child: ReducerProtocol >: ReducerProtocol { let toChildState: WritableKeyPath<ParentState, Child.State> let toChildAction: CasePath<ParentAction, Child.Action> let child: Child }
— 18:05
And now we can construct and return a pullback reducer from the pullback method: extension ReducerProtocol { public func pullback<ParentState, ParentAction>( state toChildState: WritableKeyPath<ParentState, State>, action toChildAction: CasePath<ParentAction, Action> ) -> PullbackReducer<ParentState, ParentAction,Self> { PullbackReducer( toChildState: toChildState, toChildAction: toChildAction, child: self ) } }
— 18:25
And finally we can implement the reduce requirement, which basically does exactly what the current pullback method on the Reducer type does: public func reduce( into state: inout ParentState, action: ParentAction ) -> Effect<ParentAction, Never> { guard let childAction = self.toChildAction .extract(from: action) else { return .none } return self.child .reduce( into: &state[keyPath: self.toChildState], action: childAction ) .map(self.toChildAction.embed) }
— 18:58
This now compiles, and so let’s give it a spin. Let’s convert that theoretical tab-based application reducer to the new protocol style. Rather than defining a bunch of file-scoped types for the domain of each tab along with a file-scope reducer, we will instead have a single type defined that contains the domain and reducer for each tab: struct TabA: ReducerProtocol { struct State {} enum Action {} func reduce( into state: inout State, action: Action ) -> Effect<Action, Never> { .none } } struct TabB: ReducerProtocol { struct State {} enum Action {} func reduce( into state: inout State, action: Action ) -> Effect<Action, Never> { .none } } struct TabC: ReducerProtocol { struct State {} enum Action {} func reduce( into state: inout State, action: Action ) -> Effect<Action, Never> { .none } }
— 19:59
Then we will gather all of those domains into a single, app-level domain: struct AppState { var tabA: TabA.State var tabB: TabB.State var tabC: TabC.State } enum AppAction { case tabA(TabA.Action) case tabB(TabB.Action) case tabC(TabC.Action) }
— 20:10
And finally construct a reducer that encapsulates the behavior of each tab in a single package: let appReducer = TabA() .pullback( state: \AppState.tabA, action: /AppAction.tabA ) .combine( with: TabB() .pullback( state: \AppState.tabB, action: /AppAction.tabB ) ) .combine( with: TabC() .pullback( state: \AppState.tabC, action: /AppAction.tabC ) )
— 20:54
There’s a few weird things about this, and we will get to that in a moment, but let’s look at the type of this thing: let appReducer: CombineReducer< CombineReducer< PullbackReducer<AppState, AppAction, TabA>, PullbackReducer<AppState, AppAction, TabB> >, PullbackReducer<AppState, AppAction, TabC> >
— 21:08
We are building up a super complex, deeply-nested type that statically describes how the behavior of our application is composed together. When compiled with optimizations, Swift can eliminate most of this nesting, but retaining the static typing will be incredibly useful.
— 21:23
Now, back to the weirdness. There are 2 main things that are weird about appReducer ’s current definition. First of all we are using the combine(with:) operator to combine multiple reducers together, which causes the first reducer to have more prominence in the expression since it isn’t obscured by combine(with:) noise. This is probably not the way you are used to combining reducers.
— 21:45
Instead you probably prefer to do something like this: let appReducer = CombineReducers( TabA() .pullback( state: \AppState.tabA, action: /AppAction.tabA ), TabB() .pullback( state: \AppState.tabB, action: /AppAction.tabB ), TabC() .pullback( state: \AppState.tabC, action: /AppAction.tabC ) )
— 22:04
…where you have a single top-level CombineReducers type that could hopefully take a variadic list of reducers and combine them all together.
— 22:11
However, such a type is not possible to define in Swift today. For we would want it to look something like this: struct CombineReducers<<#???#>>: ReducerProtocol { let reducers: <#???#> }
— 22:32
It somehow needs to be generic over every type of reducer we are combining, and we need to have some kind of collection of all of those reducers.
— 22:41
This currently is not a problem in the library with the Reducer struct type because there’s just the one single static type. But in this ReducerProtocol world we have an entire zoo of types that can conform to the protocol to become a reducer.
— 23:03
The other problem is that somehow we’ve ended up with a file-scope defined reducer again. One of the main reasons to introduce the ReducerProtocol was to move away from this pattern since we know it can cause strain on the compiler’s ability to perform type inference, autocomplete and various diagnostics.
— 23:22
Let’s try moving this reducer to be a proper type that conforms to the protocol: struct AppReducer: ReducerProtocol { }
— 23:38
Now we can move AppState and AppAction into the type and drop the App prefix: struct AppReducer: ReducerProtocol { struct State { var tabA: TabA.State var tabB: TabB.State var tabC: TabC.State } enum Action { case tabA(TabA.Action) case tabB(TabB.Action) case tabC(TabC.Action) } }
— 23:43
But in order to implement the reduce method while taking advantage of the combine and pullback operators we have to do something really strange. We have to construct that composed reducer, and then immediately run it on the state and action: func reduce( into state: inout State, action: Action ) -> Effect<Action, Never> { TabA() .pullback(state: \State.tabA, action: /Action.tabA) .combine( with: TabB() .pullback( state: \State.tabB, action: /Action.tabB ) ) .combine( with: TabC() .pullback( state: \State.tabC, action: /Action.tabC ) ) .reduce(into: &state, action: action) }
— 24:08
Or, we could extract the “core” logic out to a little static let defined on the type, and then reference that: func reduce( into state: inout State, action: Action ) -> Effect<Action, Never> { Self.core .reduce(into: &state, action: action) } static let core = TabA() .pullback(state: \State.tabA, action: /Action.tabA) .combine( with: TabB() .pullback(state: \State.tabB, action: /Action.tabB) ) .combine( with: TabC() .pullback(state: \State.tabC, action: /Action.tabC) )
— 24:26
Both options are quite weird, and luckily for us there’s a really great solution to all of these problems, but that is going to have to wait a bit longer. Refactoring leaf features
— 24:36
There’s still more work to be done to the ReducerProtocol before it’s ready for prime time, but before we get to that we can already start to see concrete benefits to this new style of constructing reducers. Let’s take a look at one of the demo applications in the library’s repo to see how it can benefit. We will just perform a very naive translation from its current closure-based reducers to the new protocol conformance style, and from just that we will see lots of benefits, and there are even more wins to be had. Along the way we will also come across a few other powerful reducer operators that we need to convert over to the protocol-style.
— 25:13
We will be looking at the voice memos demo application. It’s a pretty intricate demo:
— 25:21
We can start a new voice memo recording after giving permission, and talk for a few seconds.
— 25:29
Then we can hit stop and we see an item has been added to the list.
— 25:31
We can edit the title of the memo, and we can replay the voice memo, and we can even delete it.
— 25:44
This demo needs to juggle two complex dependencies: one for recording audio from the device and another for playing back audio. Further, the entire demo is separated into 3 core domains that can be run and tested in isolation, but are pieced together to form the entire application:
— 26:05
First there’s a domain for handling recording a new voice memo. It’s responsible for interacting with the audio recording dependency, running a timer in order to show the recording time, and finalizing the recording by encoding it and saving it to a temporary directory.
— 26:19
Then there’s a domain for just a single row in the list, which is responsible for playing back a voice memo, including showing the progress bar in the row.
— 26:29
And finally there’s a domain that not only glues together the two previously mentioned domains, but layers on some additional logic such as asking for recording permissions and deleting voice memos.
— 26:41
Let’s see what it takes to update this demo to use the new ReducerProtocol . We don’t need to understand how the logic of these features is implemented. We are going to be able to naively move the logic from one place to another with no changes. This will even be how you can upgrade your existing reducers to the new style in your own applications.
— 27:03
Let’s start by looking at a leaf node of the application, such as the memo recording feature, which is responsible for recording a new voice memo. We can get a stub of a type into place that will represent the reducer: struct RecordingMemo: ReducerProtocol { }
— 27:19
And we can copy the domain’s state and action types into this new type, but now we can drop the RecordingMemo prefix: struct RecordingMemo: ReducerProtocol { struct State: Equatable { … } enum Action: Equatable { … enum DelegateAction: Equatable { case didFinish(TaskResult<State>) } } }
— 27:32
We can even unnest the DelegateAction and Mode enums since the RecordingMemo type provides a nice namespace for it: struct RecordingMemo: ReducerProtocol { struct State: Equatable { … } enum Mode { … } enum Action: Equatable { … } enum DelegateAction: Equatable { … } }
— 27:44
Further, we can completely avoid the RecordingMemoEnvironment type, and instead inline those properties directly into the RecordingMemo reducer: struct RecordingMemo: ReducerProtocol { … var audioRecorder: AudioRecorderClient var mainRunLoop: AnySchedulerOf<RunLoop> }
— 27:54
And we can implement the reduce requirement by just copying and pasting the existing reducer’s body, and the only change that needs to be made is to replace all references of the environment to self .
— 28:34
Amazingly this is already compiling. This shows just how easy it will be to naively convert certain kinds of reducers to the new protocol style. There may be additional tweaks and changes you want to make once you are in the new style, but at the very least getting your foot in the door is quite straightforward.
— 28:51
Also we are already seeing a benefit to moving to the new style: there’s an unused variable warning that was previously not being surfaced to us, presumably because the compiler struggles with file-scope defined values: Immutable value ‘startRecording’ was never used; consider replacing with ‘_’ or removing it
— 29:06
This is happening due to a little trick we are employing to fire off an asynchronous task without suspending while still staying in the structured concurrency world: async let startRecording: Void = await send( .audioRecorderDidFinish( TaskResult { try await self.audioRecorder.startRecording(url) } ) )
— 29:15
The startRecording endpoint on the audio recorder client suspends for the duration of recording the voice memo. But, while we are recording we want to also start a timer so that we can show how much time has elapsed while recording.
— 29:27
We could of course fire up a task group so that we can run these two tasks in parallel, but there’s a fun little trick we can employ. We can use async let to start the recording, which doesn’t for us to suspend right at that moment, and so that allows us to then start up the timer immediately after.
— 29:44
But the critical detail to know about this is that async let is still a tool of structured concurrency. This means the scope of the closure passed to .run lives for as long as the recording is happening, and further means that if the run ’s asynchronous context is cancelled, it will also cancel the recording.
— 30:04
This is in stark contrast to spinning up new tasks using the Task initializer: Task { await send( .audioRecorderDidFinish( TaskResult { try await self.audioRecorder.startRecording(url) } ) ) }
— 30:10
Such an operation exits us from the structured concurrency world, and hence does not extend the lifetime of the run closure and does not participate in cooperative cancellation. If you want to know more about these topics we highly recommend you check out our series of episodes on Swift concurrency.
— 30:26
So, that all sounds good, but the act of using async let means we have bound a variable, and Swift is complaining we aren’t using it, even though it is void. This is considered a bug in the compiler, and technically we should even be allowed to do: async let _ = send(…)
— 30:50
…but this does not currently compile, though hopefully someday soon it will.
— 30:53
Until then we need to add an explicit await at the end of the scope just to get rid of the warning: async let startRecording: Void = … … await startRecording
— 31:11
Before deleting the old style domain and reducer, let’s use refactoring tools to rename the existing instances of state and action to refer to the state and action types housed inside the RecordingMemo type, by inserting a simple dot.
— 31:35
Since things are compiling, we can now delete the old style reducer and domain. Before doing that we can use Xcode’s refactor tool to rename all occurrences of state and action in the view and now we can delete all the old-style reducer code.
— 32:00
With that everything in this file is compiling, but we do have some compiler errors in other files to fix.
— 32:09
Before getting to that there is a small ergonomic improvement we can make to our view. If we look at the view: struct RecordingMemoView: View { let store: Store<RecordingMemo.State, RecordingMemo.Action> … }
— 32:14
…we will see that we specify a store by specify the state and action types, both of which are namespaced by the reducer type, RecordingMemo . This means we can introduce a convenience type alias for specifying a Store from a reducer: public typealias StoreOf<R: ReducerProtocol> = Store<R.State, R.Action>
— 32:41
And now the view becomes: struct RecordingMemoView: View { let store: StoreOf<RecordingMemo> … }
— 32:44
This is a lot more concise, so we are finding even more ways this reducer protocol is going to help us out.
— 32:53
The only compilers errors now are back in the VoiceMemos.swift file, which is the root feature that glues everything together.
— 33:01
The are errors due to the fact that we are referring to a recordingMemoReducer variable, which no longer exists, as well as a RecordingMemoEnvironment , which also does not exist. We are going to do the bare minimum to get this to compile for right now because eventually we will want to rewrite this entire reducer composition, but that will have to wait until the voiceMemoReducer is also converted.
— 33:16
So, what we can do is create a brand new Reducer struct instance which is defined by a closure by just calling out to the new RecordingMemo reducer under the hood: Reducer { state, action, environment in RecordingMemo( audioRecorder: environment.audioRecorder, mainRunLoop: environment.mainRunLoop ) .reduce(into: &state, action: action) }
— 33:49
And we can get that environment of dependencies from the pullback, but rather than packaging up the dependencies in a RecordingMemoEnvironment we will just use a tuple: Reducer { state, action, environment in RecordingMemo( audioRecorder: environment.audioRecorder, mainRunLoop: environment.mainRunLoop ) .reduce(into: &state, action: action) } .optional() .pullback( state: \.recordingMemo, action: /VoiceMemosAction.recordingMemo, environment: { ( audioRecorder: $0.audioRecorder, mainRunLoop: $0.mainRunLoop ) } ),
— 34:17
This gets the job done, but as we said a moment ago, this is only temporary as we progress through the refactor. We would never suggest actually writing production code like this.
— 34:33
But, with those few changes things are compiling now, and everything should work exactly as it did before. We haven’t made any real changes to the application’s logic. We’ve just been moving code around.
— 34:43
Let’s move on to the next feature: the voice memo feature. This is the feature responsible for playing back the audio for a voice memo. We will again start by defining a new type to house the reducer logic: struct VoiceMemo: ReducerProtocol { }
— 35:01
And we will move all of the domain into the type and flatten some nesting: struct VoiceMemo: ReducerProtocol { struct State: Equatable, Identifiable { … } enum Mode: Equatable { … } enum Action: Equatable { … } var audioPlayer: AudioPlayer var mainRunLoop: AnySchedulerOf<RunLoop> }
— 35:24
And we’ll move all of the reducer logic into the reduce method, and we will need to replace all occurrences of environment with self .
— 35:34
One other small “improvement” we could make to this reducer is to move the PlayID , which is used for cancelling the audio player, out of the reduce method and up to be a private type inside the reducer: struct VoiceMemo: ReducerProtocol { … private enum PlayID {} … } This may make people feel a little more comfortable, as defining types in function scopes isn’t super common in Swift, though there is nothing wrong with it!
— 35:59
Things are now compiling, and again we have unused warnings appearing that the compiler was previously hiding from us. One is similar to the one we fixed a moment ago, where we use async let to create a structured concurrent task that does not suspend: async let playAudio: Void = send( .audioPlayerClient( TaskResult { try await self.audioPlayer.play(url) } ) ) Immutable value ‘playAudio’ was never used; consider replacing with ‘_’ or removing it
— 36:13
The fix for this is to again await the value at the end of the scope: return .run { [url = state.url] send in … await playAudio } But also know the fact that we even have to do this is considered a bug in the Swift compiler and hopefully will be fixed soon.
— 36:20
And the other warning is due to us binding the progress value but never using: case let .playing(progress: progress): state.mode = .playing(progress: time / state.duration) Immutable value ‘progress’ was never used, consider replacing with ‘_’ or removing it Looks like this can simply be: case .playing: state.mode = .playing(progress: time / state.duration)
— 36:28
Everything is now compiling without any warnings, so we can delete the old style reducer and domain. But, before doing that let’s rename all old references to the domain to the new style.
— 36:54
And now we can delete all of the old style code.
— 37:01
And we can shorten the view’s store type: struct VoiceMemoView: View { let store: StoreOf<VoiceMemo> … }
— 37:12
Everything in this file is compiling, and the only errors are back in the VoiceMemos.swift file. We have the spot where we are combining the voiceMemoReducer , which no longer exists, into the main application reducer. We can do something similar to what we did for RecordingMemo where we open up a new Reducer closure in order to invoke the VoiceMemo reducer under the hood: Reducer { state, action, environment in VoiceMemo( audioPlayer: environment.audioPlayer, mainRunLoop: environment.mainRunLoop ) .reduce(into: &state, action: action) } .forEach( state: \.voiceMemos, action: /VoiceMemosAction.voiceMemo(id:action:), environment: { ( audioPlayer: $0.audioPlayer, mainRunLoop: $0.mainRunLoop ) } ),
— 38:02
It gets the job done, but again we will be making all of this a lot nicer soon enough.
— 38:15
We are finally down to the last feature of the application, which is the “voice memos” feature that glues together all the child features and layers on additional logic. Let’s start by getting a type in place that will encapsulate the feature’s logic: struct VoiceMemos: ReducerProtocol { }
— 38:38
And we will copy-and-paste the domain types into this new type: struct VoiceMemos: ReducerProtocol { struct State: Equatable { … } enum RecorderPermission { … } enum Action: Equatable { … } var audioPlayer: AudioPlayerClient var audioRecorder: AudioRecorderClient var mainRunLoop: AnySchedulerOf<RunLoop> var openSettings: @Sendable () async -> Void var temporaryDirectory: @Sendable () -> URL var uuid: @Sendable () -> UUID }
— 38:55
Next we need to implement the reduce endpoint of the type: func reduce( into state: inout State, action: Action ) -> Effect<Action, Never> { <#code#> }
— 38:58
We can do something similar to what we did in the previous two features, where we essentially just paste the old-style reducer’s code into the reduce method. The only catch this time is that the voiceMemosReducer is not just a simple reducer closure but rather a composition of 3 reducers: the RecordingMemo reducer, the VoiceMemo reducer, and then the additional reducer of logic layered on top: let voiceMemosReducer = Reducer< VoiceMemosState, VoiceMemosAction, VoiceMemosEnvironment >.combine( … )
— 39:23
We can actually move this whole expression to the reduce method, rename all occurrences of VoiceMemosState and VoiceMemosAction to just State and Action , which is nice that we can leverage type inference, and we will also have to remember to call run on it at the very end in order to actually execute it: func reduce( into state: inout State, action: Action ) -> Effect<Action, Never> { Reducer<State, Action, VoiceMemosEnvironment>.combine( … ) .run(&state, action, <#VoiceMemosEnvironment#>) }
— 39:47
But there are a few things that need to be fixed. First of all, we are getting rid of all environments, so we don’t want to make use of VoiceMemosEnvironment . So, for now, let’s just stick a Void value in for this environment: func reduce( into state: inout State, action: Action ) -> Effect<Action, Never> { Reducer<State, Action, Void>.combine( … ) .run(&state, action, ()) }
— 40:06
Now we have a bunch of new compiler errors, mostly dealing with environments since we just threw a wrench in the reducer by making the environment Void . However, the VoiceMemos type has all the dependencies this feature needs to do its job, so we can just take them from self instead of some environment.
— 40:27
For example, previously when we did some weird gymnastics to pullback the RecordingMemo ’s environment to a tuple, we can now just take those dependencies from self and don’t have to worry about pulling back the environment at all: Reducer { state, action, _ in RecordingMemo( audioRecorder: self.audioRecorder, mainRunLoop: self.mainRunLoop ) .reduce(into: &state, action: action) } .optional() .pullback( state: \.recordingMemo, action: /Action.recordingMemo, environment: {} ),
— 41:01
And similarly for the VoiceMemo reducer: Reducer { state, action, _ in VoiceMemo( audioPlayer: self.audioPlayer, mainRunLoop: self.mainRunLoop ) .reduce(into: &state, action: action) } .forEach( state: \.voiceMemos, action: /Action.voiceMemo(id:action:), environment: {} ), Further, every mention of environment can now be renamed to self . Refactoring combined features
— 41:25
So we’ve seen that it’s pretty straightforward to convert leaf features from the old style of reducer to a new reducer protocol conformance, and we started seeing benefits immediately, including better compiler diagnostics, type inference, cleanup and more. But converting reducers that compose other reducers is not so straightforward.
— 41:53
Although all of the reducers defined in the project now conform to ReducerProtocol , the root-level one that is responsible for combining the leaf features together is still secretly using Reducer structs under the hood because it depends on reducer operators that don’t yet exist on the protocol.
— 42:17
The voice memos reduce function is currently quite a lot: there is still a lot of mentions of the Reducer struct: func reduce( into state: inout State, action: Action ) -> Effect<Action, Never> { Reducer<State, Action, Void>.combine( Reducer { state, action, _ in … } … Reducer { state, action, _ in … } … ) … }
— 42:31
To fully move over to the new style, we need to port the optional and forEach reducers to the reducer protocol.
— 42:39
But before doing that, let’s make the final changes needed to power the application off the VoiceMemos reducer conformance. First we need to delete all of the old-style domain and reducer.
— 42:50
Before doing that will rename the state and action types like we did before and then delete the old domain and reducer.
— 43:13
And the view can now be initialized with a StoreOf using the new reducer type: struct VoiceMemosView: View { let store: StoreOf<VoiceMemos> … }
— 43:19
The only compiler error left in the VoiceMemos.swift file is where we construct the preview. Currently it is referring to the voiceMemosReducer variable, which we just deleted. Instead we want to create the store for the preview using the new VoiceMemos type, but currently our Store type can only be created using values of the Reducer struct type, not with conformances to the ReducerProtocol .
— 44:01
Eventually we will want to be able to create stores from ReducerProtocol conformances, but for now we can take a short cut by implementing an initializer that allows us to convert ReducerProtocol conformances into Reducer type values: extension Reducer { public init<R: ReducerProtocol>(_ r: R) where R.State == State, R.Action == Action { self.init { state, action, _ in r.reduce(into: &state, action: action) } } }
— 45:04
Then we can construct the store for the preview like so: struct VoiceMemos_Previews: PreviewProvider { static var previews: some View { VoiceMemosView( store: Store( initialState: VoiceMemos.State(…), reducer: Reducer( VoiceMemos( audioPlayer: .live, // NB: Doesn't work in previews audioRecorder: AudioRecorderClient( currentTime: { 10 }, requestRecordPermission: { true }, startRecording: { _ in try await Task.never() }, stopRecording: {} ), mainRunLoop: .main, openSettings: {}, temporaryDirectory: { URL( fileURLWithPath: NSTemporaryDirectory() ) }, uuid: { UUID() } ) ), environment: () ) ) } }
— 45:34
And we have to do something similarly for the entry point of the application: @main struct VoiceMemosApp: App { var body: some Scene { WindowGroup { VoiceMemosView( store: Store( initialState: VoiceMemos.State(), reducer: Reducer( VoiceMemos( audioPlayer: .live, audioRecorder: .live, mainRunLoop: .main, openSettings: { @MainActor in await UIApplication.shared.open( URL( string: UIApplication .openSettingsURLString )! ) }, temporaryDirectory: { URL( fileURLWithPath: NSTemporaryDirectory() ) }, uuid: { UUID() } ) ) .debug(), environment: () ) ) } } }
— 45:52
Now the entire application compiles, and it runs exactly as it did before.
— 45:57
Of course, there’s still quite a few weird things going on in the code. Perhaps the weirdest is the mix of old-style and new-style reducer code we have in the VoiceMemos type. Just looking at this: func reduce( into state: inout State, action: Action ) -> Effect<Action, Never> { Reducer<State, Action, Void>.combine( Reducer { state, action, _ in … }, … ) … } …shows that there’s still tons of room for improvement.
— 46:06
The reason we have this mixture of old-style and new-style is because we still haven’t converted enough reducer operators over to the protocol. We are forced to transition from the protocol world to the Reducer -type world just so that we can make use of things like optional and forEach .
— 46:25
So, let’s quickly port those operators over to the protocol. We’ve already seen what it looks like to port other operators from the Reducer struct type over to the protocol-style. In particular, it involves introducing a new type to encapsulate the operator’s behavior, and then a method to return that new type.
— 46:46
So, we aren’t going to spell out the details in full here, and instead paste in the final answer. For example, to define an optional() operator on the ReducerProtocol we can do the following: extension ReducerProtocol { public func optional() -> OptionalReducer<Self> { OptionalReducer(wrapped: self) } } public struct OptionalReducer<Wrapped: ReducerProtocol>: ReducerProtocol { let wrapped: Wrapped public func reduce( into state: inout Wrapped.State?, action: Wrapped.Action ) -> Effect<Wrapped.Action, Never> { guard state != nil else { runtimeWarning( """ An "optional" reducer received an action when \ state was "nil". """ ) return .none } return self.wrapped .reduce(into: &state!, action: action) } }
— 47:21
With that we can already simplify how we transform the RecordingMemo in order to fit its domain into the root VoiceMemos domain. We can now apply the optional and pullback operators directly to the RecordingMemo value, but we now get to drop the environment transformation: Reducer { state, action, _ in RecordingMemo( audioRecorder: self.audioRecorder, mainRunLoop: self.mainRunLoop ) .optional() .pullback( state: \.recordingMemo, action: /Action.recordingMemo ) .reduce(into: &state, action: action) },
— 47:53
Now let’s do the forEach operator. Again we are not going to do this from scratch because it follows the same pattern, so let’s just paste in the final answer: extension ReducerProtocol { public func forEach<ParentState, ParentAction, ID>( state toElementsState: WritableKeyPath< ParentState, IdentifiedArray<ID, State> >, action toElementAction: CasePath< ParentAction, (ID, Action) > ) -> ForEachReducer< ParentState, ParentAction, ID, Self > { ForEachReducer( toElementsState: toElementsState, toElementAction: toElementAction, element: self ) } } public struct ForEachReducer< State, Action, ID: Hashable, Element: ReducerProtocol >: ReducerProtocol { let toElementsState: WritableKeyPath< State, IdentifiedArray<ID, Element.State> > let toElementAction: CasePath< Action, (ID, Element.Action) > let element: Element public func reduce( into state: inout State, action: Action ) -> Effect<Action, Never> { guard let (id, elementAction) = toElementAction .extract(from: action) else { return .none } if state[keyPath: toElementsState][id: id] == nil { runtimeWarning( """ A "forEach" reducer received an action when \ state contained no element with that id. """ ) return .none } return self.element .reduce( into: &state[keyPath: toElementsState][id: id]!, action: elementAction ) .map { toElementAction.embed((id, $0)) } } }
— 48:22
This allows us to simplify how we transform the VoiceMemo reducer since we can apply the .forEach directly to it and we no longer need to specify an environment transformation: Reducer { state, action, _ in VoiceMemo( audioPlayer: self.audioPlayer, mainRunLoop: self.mainRunLoop ) .forEach( state: \.voiceMemos, action: /Action.voiceMemo(id:action:) ) .reduce(into: &state, action: action) },
— 48:38
Further, we now have two transformed ReduceProtocol conformances next to each other getting wrapped up in a Reducer value just so that we can combine them. We can get rid of all that indirection and just combine them together directly on the level of the protocol: Reducer { state, action, _ in RecordingMemo( audioRecorder: self.audioRecorder, mainRunLoop: self.mainRunLoop ) .optional() .pullback( state: \.recordingMemo, action: /Action.recordingMemo ) .combine( with: VoiceMemo( audioPlayer: self.audioPlayer, mainRunLoop: self.mainRunLoop ) .forEach( state: \.voiceMemos, action: /Action.voiceMemo(id:action:) ) ) .reduce(into: &state, action: action) },
— 49:15
We are slowly chipping away at all usages of the Reducer type, but we still have a big ole Reducer value that holds the additional logic that is layered on top of the recording logic and voice memo row logic.
— 49:27
We could technically move all of this logic to its own new type that conforms to the ReducerProtocol , but then we’d have to come up with a name for it and we’d have to duplicate some things like all the dependencies. That’s going to lead to a lot of boilerplate and busy work.
— 49:42
Although normally we will be defining types to encapsulate feature logic, there are still times we will want to be able to create a little ad hoc reducer in a lightweight fashion, such as from a closure. This is particularly useful when needing to mix in some reducer logic in the middle of a larger composition, like we are doing in VoiceMemos .
— 49:59
Now, we can’t simply make Reducer conform to the ReducerProtocol because it has an environment to worry about, but we can introduce a new top-level reducer type that allows us to define a reducer via a closure. We will call it Reduce : public struct Reduce<State, Action>: ReducerProtocol { let reduce: (inout State, Action) -> Effect<Action, Never> public init( _ reduce: @escaping (inout State, Action) -> Effect<Action, Never> ) { self.reduce = reduce } public func reduce( into state: inout State, action: Action ) -> Effect<Action, Never> { self.reduce(&state, action) } }
— 50:35
This allows us to once and for all rid us of all old-style reducers because we can now combine an an ad hoc Reduce into our chain of reducers: func reduce( into state: inout State, action: Action ) -> Effect<Action, Never> { RecordingMemo( audioRecorder: self.audioRecorder, mainRunLoop: self.mainRunLoop ) .optional() .pullback( state: \.recordingMemo, action: /Action.recordingMemo ) .combine( with: VoiceMemo( audioPlayer: self.audioPlayer, mainRunLoop: self.mainRunLoop ) .forEach( state: \.voiceMemos, action: /Action.voiceMemo(id:action:) ) ) .combine( with: Reduce { state, action in … } ) .reduce(into: &state, action: action)
— 51:30
Phew, ok this is intense, but the entire voice memos application is now entirely implemented using the tools of the ReducerProtocol and it works exactly as it did before. And doing this refactor gave us a few benefits, such as better autocomplete, better compiler diagnostics, and we were able to completely eliminate the concept of an environment and instead our reducer types just hold onto the dependencies they need to do their job. Next time: composition
— 52:03
However, the final code we ended up with is… well, let’s just say not ideal.
— 52:07
We naively converted some reducer operators to the brand new ReducerProtocol , such as combine , pullback , optional and forEach , but the nice thing about using a protocol is that we obtain a new tool to completely re-imagine reducer composition. And that’s result builders.
— 52:21
Result builders give us the opportunity to reimagine what the library’s core compositional operators look like. We can remove a lot of unnecessary noise when composing lots of reducers together, we can force more correctness guarantees into the API’s usage, and we can even work around some of the compiler limitations we mentioned previously, such as variadic generics.
— 52:43
Let’s revisit that particular limitation in order to find inspiration for why we want result builders and how we should implement them…next time! Downloads Sample code 0202-reducer-protocol-pt2 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 .