Unlock This Episode
Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.
Introduction
When introducing the Composable Architecture last year we put a particular emphasis on side effects and testing. In fact, we devoted 8 entire episodes to those topics. This is because every architecture needs to have a story for side effects: it’s just a fact of life. But as soon as you introduce side effects you run the risk of destroying testability.
However, we were able to make side effects and testability live in harmony by employing an old technique that we talked about nearly two years ago: the environment. The environment provides a single place to put all of the things that can cause side effects, and we forbid ourselves from using any dependencies unless it is stored in the environment. This gives us a consistent way of accessing dependencies, and makes it trivial to swap out dependencies for controlled ones in tests, playgrounds, and we can even use mock dependencies when running the actual application if we wanted. This can be particularly useful if you don’t have internet or want to test your app in a very specific state.
However, the solution we gave is not the whole picture. It works well enough in many cases, but it has a few problems, and we can devise a much more robust and universal solution to the problem of dependencies in the Composable Architecture. In order to see this all we have to do is make a very small tweak to our reducer signature, and everything will naturally follow.
But before we get into that, let’s remind ourselves how we used the environment technique to control and test our side effects.
Subscribe to Point-Free
Access this episode, plus all past and future episodes when you become a subscriber.
Already a subscriber? Log in
Exercises
Fix the PrimeTime application so that it works with the new reducer signature we developed in this episode.
What are the differences between the reducer we defined in the episode:
(inout Value, Action, Environment) -> [Effect<Action>]
And this alternative formulation, where
Environment
has been curried to afterValue
andAction
?(inout Value, Action) -> (Environment) -> [Effect<Action>]
Update the
ComposableArchitecture
module to use this form ofReducer
. What impact does it have on behavior and ergonomics?Solution
The architecture now looks like this:
public typealias Reducer<Value, Action, Environment> = (inout Value, Action) -> (Environment) -> [Effect<Action>] public func combine<Value, Action, Environment>( _ reducers: Reducer<Value, Action, Environment>... ) -> Reducer<Value, Action, Environment> { return { value, action in let effects = reducers.map { $0(&value, action) } return { environment in effects.flatMap { $0(environment) } } } } public func pullback<LocalValue, GlobalValue, LocalAction, GlobalAction, LocalEnvironment, GlobalEnvironment>( _ reducer: @escaping Reducer<LocalValue, LocalAction, LocalEnvironment>, value: WritableKeyPath<GlobalValue, LocalValue>, action: CasePath<GlobalAction, LocalAction>, environment: @escaping (GlobalEnvironment) -> LocalEnvironment ) -> Reducer<GlobalValue, GlobalAction, GlobalEnvironment> { return { globalValue, globalAction in guard let localAction = action.extract(from: globalAction) else { return { _ in [] } } let localEffects = reducer(&globalValue[keyPath: value], localAction) return { globalEnvironment in localEffects(environment(globalEnvironment)).map { localEffect in localEffect.map(action.embed) .eraseToEffect() } } } } public func logging<Value, Action, Environment>( _ reducer: @escaping Reducer<Value, Action, Environment> ) -> Reducer<Value, Action, Environment> { return { value, action in let effects = reducer(&value, action) let newValue = value return { environment in [.fireAndForget { print("Action: \(action)") print("Value:") dump(newValue) print("---") }] + effects(environment) } } } public final class Store<Value, Action>: ObservableObject { private let reducer: Reducer<Value, Action, Any> private let environment: Any @Published public private(set) var value: Value private var viewCancellable: Cancellable? private var effectCancellables: Set<AnyCancellable> = [] public init<Environment>( initialValue: Value, reducer: @escaping Reducer<Value, Action, Environment>, environment: Environment ) { self.reducer = { value, action in let effects = reducer(&value, action) return { environment in effects(environment as! Environment) } } self.value = initialValue self.environment = environment } public func send(_ action: Action) { let effects = self.reducer(&self.value, action)(self.environment) effects.forEach { effect in var effectCancellable: AnyCancellable? var didComplete = false effectCancellable = effect.sink( receiveCompletion: { [weak self] _ in didComplete = true guard let effectCancellable = effectCancellable else { return } self?.effectCancellables.remove(effectCancellable) }, receiveValue: self.send ) if !didComplete, let effectCancellable = effectCancellable { self.effectCancellables.insert(effectCancellable) } } } public func view<LocalValue, LocalAction>( value toLocalValue: @escaping (Value) -> LocalValue, action toGlobalAction: @escaping (LocalAction) -> Action ) -> Store<LocalValue, LocalAction> { let localStore = Store<LocalValue, LocalAction>( initialValue: toLocalValue(self.value), reducer: { localValue, localAction in self.send(toGlobalAction(localAction)) localValue = toLocalValue(self.value) return { _ in [] } }, environment: self.environment ) localStore.viewCancellable = self.$value.sink { [weak localStore] newValue in localStore?.value = toLocalValue(newValue) } return localStore } }
By delaying the application of an environment to a reducer, we are given stricter guarantees that it can never be used to execute arbitrary side effects that mutate the reducer’s value. And this is because
value
has in-out semantics, and a reducer must mutate thisvalue
before opening the trailing function that has an environment in scope. Overall this formulation of a reducer is a little stricter, but also a little less ergonomic, due to the increased nesting involved.Revert any work left over from the previous exercise.
Update the
logging
higher-order reducer to make the caller provide their own printing function instead of usingprint
directly in the implementation. The API for this can allow the caller to get access to the printing function by plucking it out of the environment:public func logging<Value, Action, Environment>( _ reducer: @escaping Reducer<Value, Action, Environment>, logger: @escaping (Environment) -> (String) -> Void ) -> Reducer<Value, Action, Environment>
Note that the Swift standard library has a second
dump
function that prints into a given stream/string rather than the console.Solution
public func logging<Value, Action, Environment>( _ reducer: @escaping Reducer<Value, Action, Environment>, logger: @escaping (Environment) -> (String) -> Void ) -> Reducer<Value, Action, Environment> { return { value, action, environment in let effects = reducer(&value, action, environment) let newValue = value return [.fireAndForget { let print = logger(environment) print("Action: \(action)") print("Value:") var dumpedValue = "" dump(newValue, to: &dumpedValue) print(dumpedValue) print("---") }] + effects } }
Erase the environment from the store without resorting to a force cast. The simplest way to achieve this is to remove the
Any
s:- Remove the
environment
property from the store so that it’s not even possible to force cast it. - Replace the
reducer
property with aReducer<Value, Action, Void>
- Update the initializer,
send
, andview
methods accordingly.
Solution
The initializer has an
environment
at the ready, so all we need to do is capture it and pass it along while ignoring the environment inside.public final class Store<Value, Action>: ObservableObject { private let reducer: Reducer<Value, Action, Void> @Published public private(set) var value: Value private var viewCancellable: Cancellable? private var effectCancellables: Set<AnyCancellable> = [] public init<Environment>( initialValue: Value, reducer: @escaping Reducer<Value, Action, Environment>, environment: Environment ) { self.reducer = { value, action, _ in reducer(&value, action, environment) } self.value = initialValue } public func send(_ action: Action) { let effects = self.reducer(&self.value, action, ()) effects.forEach { effect in var effectCancellable: AnyCancellable? var didComplete = false effectCancellable = effect.sink( receiveCompletion: { [weak self] _ in didComplete = true guard let effectCancellable = effectCancellable else { return } self?.effectCancellables.remove(effectCancellable) }, receiveValue: self.send ) if !didComplete, let effectCancellable = effectCancellable { self.effectCancellables.insert(effectCancellable) } } } public func view<LocalValue, LocalAction>( value toLocalValue: @escaping (Value) -> LocalValue, action toGlobalAction: @escaping (LocalAction) -> Action ) -> Store<LocalValue, LocalAction> { let localStore = Store<LocalValue, LocalAction>( initialValue: toLocalValue(self.value), reducer: { localValue, localAction, _ in self.send(toGlobalAction(localAction)) localValue = toLocalValue(self.value) return [] }, environment: () ) localStore.viewCancellable = self.$value.sink { [weak localStore] newValue in localStore?.value = toLocalValue(newValue) } return localStore } }
- Remove the
We’ve seen that a reducer with a
Void
environment is equivalent to a reducer that takes no environment at all. Use this knowledge to further simplify the store by holding onto a function that looks like a reducer, but just doesn’t have the 3rd enviroment argument.Solution
Instead of holding onto a
Reducer
, we can hold onto a simpler function signature:public final class Store<Value, Action>: ObservableObject { private let reducer: (inout Value, Action) -> [Effect<Action>]
This simplifies the initializer where the private field is assigned:
self.reducer = { value, action in reducer(&value, action, environment) }
And the
send
method, where this field is invoked:let effects = self.reducer(&self.value, action)
The
view
method can still take a void environment.
References
Dependency Injection Made Easy
Brandon Williams & Stephen Celis • Monday May 21, 2018This is the episode that first introduced our Current
environment approach to dependency injection.
Today we’re going to control the world! Well, dependencies to the outside world, at least. We’ll define the “dependency injection” problem and show a lightweight solution that can be implemented in your code base with little work and no third party library.
Dependency Injection Made Comfortable
Brandon Williams & Stephen Celis • Monday Jun 4, 2018Let’s have some fun with the “environment” form of dependency injection we previously explored. We’re going to extract out a few more dependencies, strengthen our mocks, and use our Overture library to make manipulating the environment friendlier.
How to Control the World
Stephen Celis • Monday Sep 24, 2018Stephen gave a talk on our Environment
-based approach to dependency injection at NSSpain 2018. He starts
with the basics and slowly builds up to controlling more and more complex dependencies.
Effectful State Management: Synchronous Effects
Brandon Williams & Stephen Celis • Monday Oct 14, 2019This is the start of our series of episodes on “effectful” state management, in which we explore how to capture the idea of side effects directly in our composable architecture.
Side effects are one of the biggest sources of complexity in any application. It’s time to figure out how to model effects in our architecture. We begin by adding a few new side effects, and then showing how synchronous effects can be handled by altering the signature of our reducers.
Testable State Management: Reducers
Brandon Williams & Stephen Celis • Monday Nov 25, 2019This is the start of our series of episodes on “testable” state management, in which we explore just how testable the Composable Architecture is, effects and all!
It’s time to see how our architecture handles the fifth and final problem we identified as being important to solve when building a moderately complex application: testing! Let’s get our feet wet and write some tests for all of the reducers powering our application.