Video #204: Reducer Protocol: Composition, Part 2
Episode: Video #204 Date: Sep 12, 2022 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep204-reducer-protocol-composition-part-2

Description
The new reducer protocol has improved many things, but we’re now in an awkward place when it comes to defining them: some are conformances and some are not. We’ll fix that with inspiration from SwiftUI and the help of a new protocol feature.
Video
Cloudflare Stream video ID: 45120c3ab4cf28b2af7cd63b48451dc5 Local file: video_204_reducer-protocol-composition-part-2.mp4 *(download with --video 204)*
References
- Discussions
- SE-0215: Conform Never to Equatable and Hashable
- SE-0346: Lightweight same-type requirements for primary associated types
- Function builder cannot infer generic parameters even though direct call to
buildBlockcan - 0204-reducer-protocol-pt4
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
We’ve now got a ReducerProtocol in place along with a ReducerBuilder , and things are looking really great. We see that we can still accomplish everything we were doing in the old style, but we are getting all new tools for simplifying and organizing our features built in the library.
— 0:19
But there’s still something not quite right with our VoiceMemos reducer. It is really strange that in order to implement its reduce method we are constructing a big, composed reducer just to turn around it hit its reduce method to actually perform the work. The whole reason we are doing this strange dance is because we are trying to move away from defining our reducers as variables defined at the file-scope, which is an admirable goal since file-scope variables seem to put strain on the compiler. But it still feels like we haven’t quite cracked the ergonomics of this pattern yet.
— 0:53
It feels like there’s actually two styles of implementing reducers. First, there are the leaf node features, such as the RecordingMemo and VoiceMemo features. In those situations we aren’t composing together and integrating multiple domains. We are just implementing the bare logic for those features in a reduce method.
— 1:08
But then there are also the more complicated features that do need to integrate multiple domains together, and in those situations we prefer to compose things together using result builder syntax.
— 1:19
Amazingly, it’s possible to augment the ReducerProtocol so that we can support both of these styles in one package. Then, at the moment of conforming a type to the ReducerProtocol , we can decide which style do we want to implement. Are we implementing some simple standalone logic, or are we composing many things together?
— 1:39
Let’s see what it takes to make this happen. Inspiration from SwiftUI
— 1:43
We can take some inspiration from SwiftUI to figure out what this should look like for our reducers. SwiftUI views get a natural place to express the composition of a complex view hierarchy, which is the body property: import SwiftUI struct SomeView: View { var body: some View { VStack { Text("Login") TextField("Email", text: .constant("")) Button("Go") {} } } }
— 2:05
Note that the body property in a view is implicitly given a view builder context so you can immediately use everything that result builders offer in order to simplify the syntax inside.
— 2:18
What if the ReducerProtocol also exposed a body property that allowed us to express the composition of a bunch of reducers: 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) } var body: some ReducerProtocol { … Scope(state: \.tabA, action: /Action.tabA) { TabA() } Scope(state: \.tabB, action: /Action.tabB) { TabB() } Scope(state: \.tabC, action: /Action.tabC) { TabC() } … } }
— 2:44
This would be the natural place to do this kind of work rather than constructing a composed reducer just to immediately call .reduce on it like we currently are: func reduce( into state: inout State, action: Action ) -> Effect<Action, Never> { Self.core.reduce(into: &state, action: action) }
— 2:52
…or storing it in a file-scope variable like we used to do: let appReducer = CombineReducers { Scope(state: \AppState.tabA, action: /AppAction.tabA) { TabA() } Scope(state: \AppState.tabB, action: /AppAction.tabB) { TabB() } Scope(state: \AppState.tabC, action: /AppAction.tabC) { TabC() } }
— 3:18
Let’s explore how we might accomplish this by first reminding ourselves what the View protocol looks like in SwiftUI: public protocol View { associatedtype Body: View @ViewBuilder @MainActor var body: Self.Body { get } }
— 3:31
It’s seemingly quite simple. It has just one associated typed and then a computed property that returns a value of that associated type.
— 3:43
We almost never have to even think about the Body associated type because it gets automatically inferred by the mere act of constructing a view to return from the computed property: struct SomeView: View { var body: some View { VStack { Text("Login") TextField("Email", text: .constant("")) Button("Go") {} } } }
— 3:59
The value constructed inside the computed property is some gnarly, deeply nested generic type, but we never need to think about that type because we can simply use some View .
— 4:11
The some View type allows the compiler to see the static type information and keep track of it without us having to explicitly write it out like this: var body: VStack< TupleView<(Text, TextField<Text>, Button<Text>)> > { … }
— 4:31
We would never want to write out that type explicitly and definitely would not want to have to keep it up-to-date as we make changes to the view hierarchy.
— 4:37
So, let’s take inspiration from this for our ReducerProtocol . We are going to add yet another associated type to our protocol for the Body and have it conform to the ReducerProtocol , just like we saw in the View protocol: public protocol ReducerProtocol { associatedtype State associatedtype Action associatedtype Body: ReducerProtocol … }
— 4:52
However, unlike the View protocol we have other associated types to worry about, so we further need the Body ’s domain types to match Self ’s: public protocol ReducerProtocol { associatedtype State associatedtype Action associatedtype Body: ReducerProtocol where Body.State == State, Body.Action == Action … }
— 5:06
Now that we have the new associated type we can add a new requirement to the protocol that allows you to return a body reducer: public protocol ReducerProtocol { … @ReducerBuilder var body: Body { get } }
— 5:24
This of course creates a bunch of compiler errors because we have new requirements. What we’d like to do is be able to offer the user a choice of two ways to implement the protocol: you can either provide the reduce method for when you just want to implement simple, atomic logic, or you can provide the body property for when you need to compose a bunch of reducers together.
— 5:47
It’s worth mentioning that it is not supported to supply both requirements in a conformance. You must supply exactly one, either the reduce method or the body property. Supplying both will be considered user error.
— 5:59
This sounds quite a bit different from SwiftUI’s View protocol, after all we never make a decision of what style of view we want. We just implement the body property and that’s all there is to it, right?
— 6:11
Well, that’s not entirely true. SwiftUI does actually give a choice between two different styles, it just turns out that the 2nd style is internal to the framework and they go through great lengths to hide it from us. To get a hint of the fact that there’s something tricky happening behind the scenes in SwiftUI, let’s take a look at the header for some of the most common views in the framework.
— 6:33
For example, the Text view is made to conform to View in an extension, and sets its Body associated type to be Never : extension Text: View { public typealias Body = Never }
— 6:43
This means it’s not even possible to invoke the body property without crashing. In fact, the SwiftUI framework is even compiled in a way that omits the body property from the interface of Text entirely. It’s impossible to directly invoke the body property on a Text view: Text("").body Value of type ‘Text’ has no member ‘body’
— 7:14
However, if we cast the Text to an any View we can get around this: (Text("") as any View).body
— 7:27
And running this code results in a fatal error letting us know the body property should not be called on Text : SwiftUI/View.swift:94: Fatal error: body() should not be called on Text.
— 7:37
And Text is only one example of a view whose Body type is Never . There are 75 other views in the header that have this condition. So, what’s up with these views? They seem really strange.
— 7:53
Well, these are examples of “system views”, sometimes known as “builtin” views or even “primitive” views. They are views that are constructed at a deeper level than what is available to us. Rather than being expressed in a view builder context by piecing together other views, they implement hidden, underscored protocol methods that we don’t even see unless we know where to look.
— 8:15
We can search the .swiftinterface files that ship in Xcode in order to get access to all exposed symbols, even the ones that Xcode hides from us in the autocomplete popup: $ grep -B1 -A7 ' protocol View ' \ $(xcrun -sdk iphoneos --show-sdk-path)/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64-apple-ios.swiftinterface
— 8:32
This shows us that the true definition of SwiftUI’s View protocol is a lot more intense than what we see in the documentation: @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) @_typeEraser(AnyView) public protocol View { static func _makeView( view: SwiftUI._GraphValue<Self>, inputs: SwiftUI._ViewInputs ) -> SwiftUI._ViewOutputs static func _makeViewList( view: SwiftUI._GraphValue<Self>, inputs: SwiftUI._ViewListInputs ) -> SwiftUI._ViewListOutputs @available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) static func _viewListCount( inputs: SwiftUI._ViewListCountInputs ) -> Swift.Int? associatedtype Body: SwiftUI.View @SwiftUI.ViewBuilder @_Concurrency.MainActor(unsafe) var body: Self.Body { get } }
— 8:46
There are 3 additional requirements, all static functions that are underscored.
— 8:57
Even though Xcode won’t autocomplete these methods it will still allow us to access them: Text._makeView
— 9:19
And we can even see its arguments and return type: let tmp = Text._makeView // (_GraphValue<Text>, _ViewInputs) -> _ViewOutputs
— 9:27
So, builtin views like Text , VStack and others conform to the View protocol by implementing these lower level, more basic requirements, and then letting the Body associated type be Never with a body computed property that simply fatal errors.
— 9:43
We now see that even SwiftUI provides two very different ways to create views. There’s the low-level style that interfaces with “graph values” and view “inputs” and “outputs”, whatever those things are, and then there’s the high-level style that simply composes together a bunch of existing views.
— 10:00
This is exactly how we want to think about our ReducerProtocol . We too want to offer two choices, one that’s “low”-level and writes out reducer logic in terms of mutating some state and returning effects, and then a “high”-level version that simply composes a bunch of reducers together.
— 10:17
For example, the one concrete, “builtin” reducer the library ships with, the EmptyReducer , should be able to say that its body should not be called directly: public struct EmptyReducer<State, Action> : ReducerProtocol { … public var body: Never { fatalError( "Body of EmptyReducer should not be called." ) } }
— 10:36
However, we can’t do this because the Never type does not conform to ReducerProtocol , and we can’t make it conform even if we wanted to because we need it to be a reducer for any state and action, but we would be force to pick a specific one: extension Never: ReducerProtocol { public func reduce( into state: inout <#???#>, action: <#???#> ) -> Effect<<#???#>, Never> { <#code#> } }
— 11:00
Now technically speaking there’s nothing preventing Swift from automatically conforming Never to the ReducerProtocol , and nearly every protocol out there. After all, a Never value can’t be constructed, hence this reduce method can never be called, so what would the harm be in allowing it to masquerade as a reducer? Allowing for such a thing would make Never into what is known as a “bottom” type, which means that it can be thought of as a kind of subtype to pretty much every type out there.
— 11:34
There has been talk on the evolution forums to allow this, but unfortunately there hasn’t been any movement on making it a reality.
— 11:39
So, for the time being, we think the best way forward is to drop the constraint on the Body associated type in our protocol: public protocol ReducerProtocol { associatedtype State associatedtype Action associatedtype Body // : ReducerProtocol // where Body.State == State, Body.Action == Action … }
— 11:48
We still have a bunch of compiler errors, but it looks like the EmptyReducer is now compiling. It is now OK for us to specify that its Body associated type is Never , and so we can just make its body perform a fatalError .
— 12:19
But even better, we now have the ability to fix all compiler errors in one fell swoop. We can provide a default implementation of the body requirement for when you only specify a reduce , and conversely we can provide a default implementation of reduce for when you only specify a body .
— 12:37
For example, we can extend the protocol when the Body associated type is Never with a default implementation of the computed property, which means it’s a “primitive” reduce that can only implement the reduce method: extension ReducerProtocol where Body == Never { public var body: Body { fatalError( "Body of \(Self.self) should not be called." ) } }
— 12:57
Which means we can delete the body we defined manually for EmptyReducer . // public var body: Never { … }
— 13:03
And it still compiles, so Swift is smart enough to infer the body as Never automatically.
— 13:11
And conversely, we can extend the ReducerProtocol when its Body also conforms to the ReducerProtocol , which remember is technically what we wanted to do from the very beginning but couldn’t due to Never not being a true “bottom” type, and provide a default reduce implementation: extension ReducerProtocol where Body: ReducerProtocol, Body.State == State, Body.Action == Action { public func reduce( into state: inout Body.State, action: Body.Action ) -> Effect<Body.Action, Never> { self.body.reduce(into: &state, action: action) } }
— 13:55
With just those changes we now have everything compiling, and we get to choose how we want to implement our reducers. We can either conform by providing the reduce method like we do in the Counter reducer: struct Counter: ReducerProtocol { struct State { … } enum Action { … } func reduce( into state: inout State, action: Action ) -> Effect<Action, Never> { … } }
— 14:12
…or we can construct a reducer by specifying a body computed property that returns a composition of reducers, just like we are doing in the tab-based application reducer: 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) } var body: some ReducerProtocol { Scope(state: \State.tabA, action: /Action.tabA) { TabA() } Scope(state: \State.tabB, action: /Action.tabB) { TabB() } Scope(state: \State.tabC, action: /Action.tabC) { TabC() } } }
— 14:37
However, this doesn’t actually compile: Type ‘AppReducer’ does not conform to protocol ‘ReducerProtocol’ Primary associated types
— 14:55
We’ve gotten pretty close to achieving our goals here: we now have a reducer protocol that provides two different styles of either implementing a reduce endpoint or composing multiple reducers together, but it’s not quite compiling in practice yet.
— 15:09
This is happening because some types don’t keep track of the underlying associated types by default. If you mask the type with a some then you lose its associated types forever. If you have ever tried using some types on any moderately complex protocols, especially ones with very visible associated types, you’ve probably already run into this limitation.
— 15:36
For example, if you mask the type of a publisher and then try to map on it, you will find that the compiler has no idea what the type is of the value you are transforming: import Combine func somePublisher() { let x: some Publisher = Just(1) x.map { $0 + 1 } } Binary operator ‘+’ cannot be applied to operands of type ‘(some Publisher).Output’ and ’Int’
— 16:13
This is why the concept of “primary associated types” were introduced in Swift 5.7. They allow us to designate a subset of associated types to be visible to the outside so that their information can be carried along the opaque some type.
— 16:27
The Publisher protocol adopts primary associated types for its Output and Failure associated types, so if we specify those we suddenly are capable of mapping on it: import Combine func somePublisher() { let x: some Publisher<Int, Never> = Just(1) x.map { $0 + 1 } }
— 16:40
…and now the compiler knows when we .map on the publisher we need to transform an integer into something else.
— 16:47
This is why specifying some ReducerProtocol for the body is not enough to convince the compiler we’ve fulfilled all of the requirements. It has no idea what kind of reducer we are constructing on the inside of the property. It could be a reducer that handles completely different types of state and action, and the compiler has no idea since the associated types are hidden from it.
— 17:07
To see that this is indeed the case for our current Counters reducer, let’s specify the type of reducer being returned from the body property, which will explicitly tell the compiler what the associated types are: var body: CombineReducer< CombineReducer< CombineReducer< CombineReducer< Reduce<State, Action>, Scope<State, Action, TabA> >, Scope<State, Action, TabB> >, Scope<State, Action, TabC> >, Reduce<State, Action> > { … }
— 17:22
Now it actually compiles, but of course this is not the kind of code we want to write in practice. It’s extremely verbose and would require lots of maintenance. We’d far prefer if the compiler could infer all of this for us.
— 17:39
We need to expose more type information for some ReducerProtocol so that it can figure out the rest, and we can do this by introducing a primary associated types to the protocol. It’s as easy as adding some angled brackets to the definition of the protocol to specify which of the associated types are primary: public protocol ReducerProtocol<State, Action> { … }
— 17:58
This syntax is only possible in Swift 5.7 and later, which we have access to right now since we are running in the Xcode 14 beta.
— 18:06
Note that we are choosing to only publicize the state and action types as primary. Notably, we are leaving out the Body type. In practice, that type is some ghastly, nested type that we never want to have to specify explicitly. By making it non-primary we can continue hiding that detail from users of the protocol.
— 18:25
With that set up we can now get the App reducer work with some ReducerProtocol as long as we specify the primary associated types: struct AppReducer: ReducerProtocol { … var body: some ReducerProtocol<State, Action> { … } }
— 18:33
And this now compiles.
— 18:35
We can even introduce a small shortcut for specifying both associated types by only specifying the type of reducer: public typealias ReducerProtocolOf<R: ReducerProtocol> = ReducerProtocol<R.State, R.Action>
— 19:01
Now the body computed property shortens to: var body: some ReducerProtocolOf<Self> { … }
— 19:07
However, we aren’t actually going to do that right now because we have found a bug in Swift or Xcode, or perhaps both, that causes the editor to become flakey when this type alias is exposed. Hopefully the bug will be fixed by the time the updated library is released, but for now we will be cautious: var body: some ReducerProtocol<State, Action> { … }
— 19:27
We can now take advantage of this new style of reducer to re-implement the VoiceMemos reducer by specifying a body property rather than a reduce method. This allows us to remove the weirdness of constructing a composed reducer just so that we can invoke reduce on it: struct VoiceMemos: ReducerProtocol { … var body: some ReducerProtocol<State, Action> { Reduce { state, action in … } .ifLet( state: \.recordingMemo, action: /Action.recordingMemo ) { RecordingMemo( audioRecorder: self.audioRecorder, mainRunLoop: self.mainRunLoop ) } .forEach( state: \State.voiceMemos, action: /Action.voiceMemo(id:action:) ) { VoiceMemo( audioPlayer: self.audioPlayer, mainRunLoop: self.mainRunLoop ) } } }
— 20:01
We can even split out the “core” logic of the reducer into a little helper method, which helps make the body computed property much tidier and easier to grok: struct VoiceMemos: ReducerProtocol { … var body: some ReducerProtocol<State, Action> { Reduce(self.core) .ifLet( state: \.recordingMemo, action: /Action.recordingMemo ) { RecordingMemo( audioRecorder: self.audioRecorder, mainRunLoop: self.mainRunLoop ) } .forEach( state: \.voiceMemos, action: /Action.voiceMemo(id:action:) ) { VoiceMemo( audioPlayer: self.audioPlayer, mainRunLoop: self.mainRunLoop ) } } func core( state: inout State, action: Action ) -> Effect<Action, Never> { … } }
— 20:52
This is starting to look really, really nice.
— 21:04
Let’s revisit the App reducer for the tab-based application.
— 21:20
We might assume that we can use these new primary associated types to eliminate some of the explicit generics. However, we cannot currently drop the Reduce generics, or the root type name for the key paths in the Scope s: struct AppReducer: ReducerProtocol { … var body: some ReducerProtocol<State, Action> { Reduce { _, _ in … } Scope(state: \.tabA, action: /Action.tabA) { TabA() } Scope(state: \.tabB, action: /Action.tabB) { TabB() } Scope(state: \.tabC, action: /Action.tabC) { TabC() } Reduce { _, _ in … } } } Cannot infer key path type from context; consider explicitly specifying a root type
— 21:25
This seems a little strange. Surely the compiler knows that we are dealing with State inside the body , after all we are specifically calling out: var body: some ReducerProtocol<State, Action> { … }
— 21:33
So, what gives? Result builder type inference
— 21:34
Things are looking good, but there’s still room for improvement. While the body computed property does allow us to describe complex compositions of reducers, it does have some trouble with type inference.
— 21:46
Luckily, we can employ a couple of advanced techniques of result builders to fix the problem and make type inference in this new style of reducer composition even better than it was in the old.
— 22:00
The problem is that each statement in a builder context is fully evaluated as a standalone statement before it is fed to the buildPartialBlock functions we defined earlier. So, when you see something like this: var body: some ReducerProtocol<State, Action> { Scope(state: \.tabA, action: /Action.tabA) { TabA() } Scope(state: \.tabB, action: /Action.tabB) { TabB() } … }
— 22:23
…behind the scenes it is turned into this: let statement1 = Scope( state: \.tabA, action: /App.Action.tabA ) { TabA() } let block1 = ReducerBuilder .buildPartialBlock(first: statement1) let statement2 = Scope( state: \.tabB, action: /App.Action.tabB ) { TabB() } let block2 = ReducerBuilder.buildPartialBlock( accumulated: block1, next: statement2 ) …
— 23:23
Notice that the statement1 variable can’t possible type check because there is not enough type information to know what root \.tabA refers to, so we get the exact same compiler error: Cannot infer key path type from context; consider explicitly specifying a root type
— 23:39
There are a few tricks we can employ to provide better type inference for result builders. The main problem is that the statements in the builder context are evaluated in full isolation, and so we can’t possibly omit any type information since there is no surround context for it to be inferred from.
— 23:55
Result builders provide a tool to help with this, and it’s called buildExpression . If you implement a buildExpression static method in a @ResultBuilder type: @resultBuilder public enum ReducerBuilder { … public static func buildExpression( _ expression: <#Expression#> ) -> <#Component#> { <#code#> } }
— 24:10
…then each statement in the builder context will be wrapped in this function: let statement1 = ReducerBuilder.buildExpression( Scope(state: \.tabA, action: /App.Action.tabA) { TabA() } ) let block1 = ReducerBuilder .buildPartialBlock(first: statement1) …
— 24:25
So, this now gives us a way to surround our statements in a context that we control, but we further need type information in this context. How can we do that?
— 24:37
We can add generics to the ReducerBuilder type itself, which will help us propagate types down to buildExpression and hence down to each statement in builder context: let statement1 = ReducerBuilder< AppReducer.State, AppReducer.Action > .buildExpression( Scope(state: \.tabA, action: /AppReducer.Action.tabA) { TabA() } ) let block1 = ReducerBuilder< AppReducer.State, AppReducer.Action > .buildPartialBlock(first: statement1) …
— 25:04
So, let’s hop up to the ReducerBuilder definition and add generics for the state and action types we are building over: public enum ReducerBuilder<State, Action> { … }
— 25:11
This is going to break a number of things, but before moving onto that we want to first pin the generics in each buildPartialBlock to match the generics of the builder type, which can be done in a nice succinct syntax now that we have primary associated types: public static func buildPartialBlock< R: ReducerProtocol<State, Action> >(first: R) -> R { first } public static func buildPartialBlock< R0: ReducerProtocol<State, Action>, R1: ReducerProtocol<State, Action> >(accumulated: R0, next: R1) -> CombineReducer<R0, R1> { .init(lhs: accumulated, rhs: next) }
— 26:07
And we can implement the buildExpression static method as something that takes a reducer and just returns it since the only purpose for buildExpression is to help with type inference. We don’t actually need it to do any work: public static func buildExpression< R: ReducerProtocol<State, Action> >(_ expression: R) -> R { expression }
— 26:10
Then, anywhere we specify @ResultBuilder we now need to also specify its generics. For example, in the CombineReducers type: public struct CombineReducers< R: ReducerProtocol >: ReducerProtocol { public init( @ReducerBuilder<R.State, R.Action> build: () -> R ) { self.reducer = build() } … }
— 26:24
We can even introduce a convenience type alias that will help shorten this: public typealias ReducerBuilderOf<R: ReducerProtocol> = ReducerBuilder<R.State, R.Action>
— 26:50
…which allows us to simply write: public struct CombineReducers< R: ReducerProtocol >: ReducerProtocol { public init(@ReducerBuilderOf<R> build: () -> R) { self.reducer = build() } … }
— 26:55
There’s a few other spots to fix: public protocol ReducerProtocol<State, Action> { … @ReducerBuilder<State, Action> var body: Body { get } … } … public struct Scope< State, Action, Child: ReducerProtocol >: ReducerProtocol { … public init( … @ReducerBuilderOf<Child> _ local: () -> Local ) { … } … } … extension ReducerProtocol { public func ifLet<Child: ReducerProtocol>( … @ReducerBuilderOf<Child> then child: () -> Child ) -> IfLetReducer<Self, Child> { … } } … extension ReducerProtocol { public func forEach< ID: Hashable, Element: ReducerProtocol >( … @ReducerBuilderOf<Element> _ element: () -> Element ) -> ForEachReducer<Self, ID, Element> { … } }
— 27:17
And now everything is compiling. Even that de-sugared builder code where we showed off how are reducer is turned into multiple invocations of the build methods: let statement1 = ReducerBuilder<App.State, App.Action> .buildExpression( Scope(state: \.tabA, action: /App.Action.tabA) { TabA() } ) let block1 = ReducerBuilder<App.State, App.Action> .buildPartialBlock(first: statement1) let statement2 = ReducerBuilder<App.State, App.Action> .buildExpression( Scope(state: \.tabB, action: /App.Action.tabB) { TabB() } ) let block2 = ReducerBuilder< App.State, App.Action > .buildPartialBlock(accumulated: block1, next: statement2)
— 27:42
And we can now leverage more type inference in our App reducer by dropping the root types in the key paths for scoping: var body: some ReducerProtocol<State, Action> { Scope(state: \.tabA, action: /Action.tabA) { TabA() } Scope(state: \.tabB, action: /Action.tabB) { TabB() } Scope(state: \.tabC, action: /Action.tabC) { TabC() } }
— 28:10
This fixes all of our type inference woes when dealing with builders, and we think it even gives a better experience over the old style of composing reducers. For example, in the old style, there were many different, seemingly random decisions we could make when specifying the generics for a reducer. So long as we make at least one instance of AppState , one instance of AppAction , and one instance of AppEnvironment explicit, the rest can be inferred. let appReducer = Reducer< AppState, AppAction, AppEnvironment >.combine( Reducer< AppState, AppAction, AppEnvironment > { _, _, _ in .none }, tabAReducer .pullback( state: \AppState.tabA, action: /AppAction.tabA, environment: { (_: AppEnvironment) in .init() } ), … )
— 28:43
For instance, we could simply specify all the types up front, and omit them elsewhere: let appReducer = Reducer< AppState, AppAction, AppEnvironment >.combine( Reducer { _, _, _ in .none }, tabAReducer .pullback( state: \.tabA, action: /AppAction.tabA, environment: { _ in .init() } ), … )
— 28:56
But we had a lot of other possibilities, too, we could sprinkle all or some of these types deep in the composition, randomly: let appReducer = Reducer< _, _, AppEnvironment >.combine( Reducer<AppState, _, _> { _, _, _ in .none }, tabAReducer .pullback( state: \.tabA, action: /AppAction.tabA, environment: { _ in .init() } ), … )
— 29:19
There are just a lot of combinations out there, and nothing to ground us in a single style.
— 29:25
What’s nice about the protocol style is that we specify the types a single time in a consistent manner: var body: some ReducerProtocol<State, Action> { … }
— 29:36
Once that is done the types flow throughout the entire builder context and we can elide many of the explicit types. Next time: dependencies
— 29:43
We have now made huge improvements to the ergonomics of defining reducers, which make it simpler and more natural to build features with the Composable Architecture. We now implement reducers as types that conform to the ReducerProtocol , and we can do so in one of two ways: by implementing a reduce method that mutates state whenever an action comes into the system, or by implementing a body computed property that expresses how to compose a bunch of reducers together.
— 30:06
Most importantly, everything we have added to the library is still 100% backwards compatible. Every existing Composable Architecture application will still compile and run exactly as it did before these changes.
— 30:18
Further, even though we are using some advanced Swift 5.7 features to make reducer builders and bodies as ergonomic as possible, we can approximate these tools for those who need to stay on Swift 5.6 for a bit longer. This means if you can’t immediately upgrade your project to Xcode 14, you can still write reducers in this style, with just a few small changes. We aren’t going to cover those details right now, but just know that it will be available in the final library release.
— 30:45
But there are more benefits to be had from this new style of defining reducers. We have already completely removed the concept of “environment” from reducers, and instead just hold onto dependencies directly in the conforming type itself, but now we can start to explore more ways to simplify dependency management. What if we could adopt a style similar to SwiftUI’s environment values, where instead of explicitly passing values throughout a view hierarchy, you can have them globally and implicitly available, and then any view can grab ahold of the value whenever they want.
— 31:16
This comes with a ton of benefits. First of all parent views do not need to hold onto dependencies it doesn’t need just so that child views have access to them. We also eliminate the need to create public initializers when modularizing our application just so that we can pass dependencies from one module to another. And we make it easy to override just a single dependency in a child feature, which can be great for running a feature in an alternative environment.
— 31:43
Let’s start by theorizing what this feature could look like by taking some inspiration from SwiftUI. We’ll take a look at the voice memos demo since that has been converted to the protocol style and it has some interesting dependencies…next time! References SE-0215: Conform Never to Equatable and Hashable Matt Diephouse • May 24, 2018 The Swift evolution proposal that extended Never to conform to Equatable and Hashable . The Never type, sometimes called a “bottom” type, cannot be instantiated, and can theoretically conform to most protocols. https://github.com/apple/swift-evolution/blob/ec2028964daeda2600e49aa89fd9e59d2363433b/proposals/0215-conform-never-to-hashable-and-equatable.md SE-0346: Lightweight same-type requirements for primary associated types Pavel Yaskevich, Holly Borla, Slava Pestov • Mar 11, 2022 The Swift evolution proposal that introduced primary associated types to protocols, which are what unlocked the Composable Architecture’s ability to allow reducer compositions to be more ergonomically defined. https://github.com/apple/swift-evolution/blob/9544e17966879e4a492f0924ab2e6a6e31748225/proposals/0346-light-weight-same-type-syntax.md Function builder cannot infer generic parameters even though direct call to buildBlock can Dan Zheng, Holly Borla, Doug Gregor, et al. • Apr 26, 2020 A forum discussion about result builder generic inference and how it differs from the rest of Swift. https://forums.swift.org/t/function-builder-cannot-infer-generic-parameters-even-though-direct-call-to-buildblock-can/35886/5 Downloads Sample code 0204-reducer-protocol-pt4 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 .