EP 91 · Standalone · Feb 17, 2020 ·Members

Video #91: Dependency Injection Made Composable

smart_display

Loading stream…

Video #91: Dependency Injection Made Composable

Episode: Video #91 Date: Feb 17, 2020 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep91-dependency-injection-made-composable

Episode thumbnail

Description

While we love the “environment” approach to dependency injection, which we introduced many episodes ago, it doesn’t feel quite right in the Composable Architecture and introduces a few problems in how we manage dependencies. Today we’ll make a small tweak to the architecture in order to solve them!

Video

Cloudflare Stream video ID: 4e7df6ec13a2fdbfbe71d7f622572f2a Local file: video_91_dependency-injection-made-composable.mp4 *(download with --video 91)*

References

Transcript

0:40

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.

0:54

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.

1:27

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.

2:06

But before we get into that, let’s remind ourselves how we used the environment technique to control and test our side effects. Effects recap

2:17

Recall that the way we express side effects in the Composable Architecture is to return an array of effect values from our reducers, and then behind the scenes the store is responsible for getting all of those effects and running them. public typealias Reducer<Value, Action> = (inout Value, Action) -> [Effect<Action>]

3:00

An Effect is a custom type that we built, which conforms to the Publisher protocol from the Combine framework: public struct Effect<Output>: Publisher { public typealias Failure = Never let publisher: AnyPublisher<Output, Failure> public func receive<S: Subscriber>(subscriber: S) where Failure == S.Failure, Output == S.Input { self.publisher.receive(subscriber: subscriber) } }

3:08

This allows us to leverage Combine for the heavy lifting of our effects. We can make use of their helpers for performing network requests, decoding JSON in models, timers, as well as all of the fun transformations you can do with publishers like map , zip , flatMap , filter and more.

3:30

Our Effect simply wraps an existing publisher, and we do this because we want to add our own convenience helpers without polluting the publisher API for everyone else. We have helpers like the fireAndForget function: extension Effect { public static func fireAndForget( work: @escaping () -> Void ) -> Effect { return Deferred { () -> Empty<Output, Never> in work() return Empty(completeImmediately: true) } .eraseToEffect() } }

3:52

This lets us fire off a side effect that needs to do some work, but doesn’t need to feed any data back into the system. Examples of this could be logging and analytics tracking, and we even used this style effect for us to save a list of numbers to disk.

4:12

We also have a helper for performing a bit of synchronous work in an effect, for those times we need to interact with the outside world, but it doesn’t necessarily need to be done asynchronously: extension Effect { public static func sync(work: @escaping () -> Output) -> Effect { return Deferred { Just(work()) } .eraseToEffect() } }

4:27

We used, for example, this to perform the effect of loading some data from disk.

4:38

And more generally, we are free to use any APIs in Combine for constructing and transforming publishers, and then at the very end of that work you can transform your publisher into an effect using the eraseToEffect helper: extension Publisher where Failure == Never { public func eraseToEffect() -> Effect<Output> { Effect(publisher: self.eraseToAnyPublisher()) } }

5:00

The way we get these effects into our application is to construct and return them from our reducers.

5:51

As a simple example, let’s say we wanted to print something in our counter view when the decrement button was tapped. We could simply return a fireAndForget effect that does the printing: public func counterReducer(state: inout CounterState, action: CounterAction) -> [Effect<CounterAction>] { switch action { case .decrTapped: state.count -= 1 return [ .fireAndForget { print(state.count) } ] Escaping closure captures ‘inout’ parameter ‘state’

6:06

This doesn’t work right now because we are trying to access the inout state variable inside an escaping closure. And this is a very good error, because if Swift allowed this it would mean that we could mutate our state in a closure that is executed at a later time. In fact, this closure could be made to execute 10 seconds from when it is created, which would mean some mysterious mutation is happening outside the purview of our reducer, and this is exactly what we want to prevent in our architecture.

6:41

However, if we get an immutable reference to the count outside the closure, everything works just fine: case .decrTapped: state.count -= 1 let count = state.count return [ .fireAndForget { print(count) } ]

7:05

What if we wanted to do something a little more complicated? What if after the decrement button is tapped, we want to wait 1 seconds, and then increment the count? That’s of course a silly thing to do, but it’s a pretty complex effect to want to handle. Luckily we can do it quite simply using the power of the Combine framework: return [ .fireAndForget { print("Decr Tapped!", count) }, Just(.incrTapped) .delay(for: 1, scheduler: DispatchQueue.main) .eraseToEffect() ] Here we wrapping up the action we want to send in a Just publisher, which is a publisher that immediately emits its value. We then delay its emission for 1 second, and finally erase the publisher to an effect.

8:00

These 3 lines are packing a punch, and means now if we run the app, each time we try to decrement it will undo that work one second later. Even if we tap it a bunch of times it will ultimately undo all of its work. Environment recap

8:31

So that’s effects. They allow us to get work done that needs to interact with the outside world, which reducers by themselves cannot do. But side effects are also notoriously difficult to test, and so in order to control our effects we turned to the environment technique.

8:53

To adopt this technique we start by defining an environment struct that holds all of the dependencies that our application needs access to. For example, the Counter module only needs access to a function for computing the “nth prime”, which typically makes a network request out to a powerful computing platform known as Wolfram Alpha: struct CounterEnvironment { var nthPrime: (Int) -> Effect<Int?> }

9:14

We make this field a var because it will make it very easy to swap out live dependencies for mock dependencies in tests, playgrounds and staged applications.

9:27

As a part of this technique we also like to provide easy access to a live and mock implementation of the environment, expressed as statics: extension CounterEnvironment { static let live = CounterEnvironment(nthPrime: Counter.nthPrime) } extension CounterEnvironment { static let mock = CounterEnvironment(nthPrime: { _ in .sync { 17 } }) }

10:07

So far none of this may seem controversial, but then we do something really weird: we define a global mutable instance of this environment that defaults to the live environment: var Current = CounterEnvironment.live

10:17

We then force ourselves to never reach out to a dependency unless it is stored in the environment. For example, when we tap the button for asking what the “nth prime” is, we execute the effect by reaching into the Current environment: case .nthPrimeButtonTapped: state.isNthPrimeButtonDisabled = true return [ Current.nthPrime(state.count) .map(CounterAction.nthPrimeResponse) .receive(on: DispatchQueue.main) .eraseToEffect() ]

10:34

This mutable variable may seem strange, but it allows us to easily mock out the entire environment in one line. For example, in the counter tests we did the following in the setUp block: class CounterTests: XCTestCase { override func setUp() { super.setUp() Current = .mock } }

10:48

This means we can guarantee that every test runs in a controlled environment, but we can also further tweak the environment in each test case by just rewriting any of the dependencies, for example: func testNthPrimeButtonHappyFlow() { Current.nthPrime = { _ in .sync { 17 } } assert( Current problems

11:19

So, that’s how the environment technique works. It’s super easy to get started with, gave us a single, consistent way to access our dependencies, and was trivial to mock all of the dependencies at once.

11:48

However, it’s not without its problems.

11:51

Perhaps the most obvious problem is that each of the environments we defined live in their own modules, and so are completely disconnected. Remember that one of the benefits to our composed reducer approach to architecture is that we could write integration-like tests that exercise multiple layers of the application at once.

12:23

For example, we wrote a test that showed that the counter feature and the prime modal feature integrated properly. In particular, we simulated the idea of a user incrementing the counter by 1, and then adding and removing that number from their favorite primes. The code looked like this: func testPrimeModal() { assert( initialValue: CounterViewState( count: 1, favoritePrimes: [3, 5] ), reducer: counterViewReducer, steps: Step(.send, .counter(.incrTapped)) { $0.count = 2 }, Step(.send, .primeModal(.saveFavoritePrimeTapped)) { $0.favoritePrimes = [3, 5, 2] }, Step(.send, .primeModal(.removeFavoritePrimeTapped)) { $0.favoritePrimes = [3, 5] } ) }

12:52

This is incredible powerful, and we basically get it for free from our composable reducers.

12:57

However, we aren’t testing any effects in this script of steps, so we aren’t seeing the full picture. Right now only the Counter module has any effects, the PrimeModal module doesn’t have any, so it’s hard to see what the complication would be here. In order to mock out all the effects, all we need to do is: func testPrimeModal() { Current = .mock assert(

13:16

But, if we back up and think about writing an integration test for our main app target, the one that composes everything together into one big appReducer , we will get a better idea of what is required. Let’s first clean up the PrimeTimeTests file: import XCTest @testable import PrimeTime class PrimeTimeTests: XCTestCase { }

13:32

If we wanted to write an integration test for this, we would first want to import all of our features: @testable import Counter @testable import FavoritePrimes @testable import PrimeModal

13:40

And we could introduce a test function: class PrimeTimeTests: XCTestCase { func testIntegration() { } }

13:44

Now, in this test we want to make sure we are in a fully controlled environment, and so we need to mock out each of our module’s environments. This means both the Counter module and the FavoritePrimes module: class PrimeTimeTests: XCTestCase { func testIntegration() { Counter.Current = .mock FavoritePrimes.Current = .mock } }

14:13

And this is a bit of a bummer. We are not getting any static help from the compiler to make sure we did this correctly. As we add more and more features to our application we will create new modules to hold the features, and each of those modules will have environments, and nothing will ever force us to make sure we have mocked out those environments. We run the risk of calling out to live dependencies during tests.

14:33

Now, that might not be the biggest problem for you and your team. There is definitely process you can put into place to help catch things like that. But it’s something to keep in mind, and something that ideally would have a more universal fix.

14:44

Another problem with this approach is that it’s not easy to share dependencies across modules. For example, the FavoritePrimes module holds a FileClient that we used to save and load data to disk. If we wanted this same dependency in another module, we’d most likely have to construct a new file client and use it in that module. Otherwise we would need to do some coordination at a higher level. For example, in the app delegate we could do some extra work to make sure that the FavoritePrimes module and whatever other module all share the same file client. We’d also have to make sure to do the same work in our tests to make sure that all the modules are being tested with the same dependencies. Again, possible to solve, but ideally there would be a more universal fix to this problem,

15:40

And finally, another problem that this form of the environment technique has is that all instances of the things inside a module can only use the one true environment in that environment. For example, we can’t create a counter view that uses the Wolfram API to do its computation, but then also create a counter view that uses some other API to do its computation. This makes the code in each of our modules a little less flexible than necessary.

16:07

The solution to all of these problems is that we should explicitly pass dependencies to the functions that need those dependencies. If you give a function everything it needs to do its job, then it is trivial to test them and control them.

16:21

However, this is not always possible or easy to do. In real world code bases it may be incredibly difficult or not even possible to pass around dependencies like you would want. You could have legacy code that makes it difficult, or you could have layers of abstraction that prevent you from doing what you want. And that’s where the environment technique really shines. It allows you to get some testability and some control of your dependencies immediately in any code base. Environment in the reducer

16:52

However, with the Composable Architecture things are a bit different. It gives us a single, consistent manner to build features across the entire application. If we can make the architecture aware of the environment, then we wouldn’t need to reach out to a global environment because we could have it available to us automatically.

17:26

The place to start doing this is the reducer, because it’s the one responsible for producing effects that are later run by the store. In order for those effects to be controllable, we need to get access to its dependencies in a controllable way. Previously we reached out to the module’s environment, but instead what if we had an environment available to us right inside the reducer?

17:48

So, instead of our reducer looking like this: public typealias Reducer<Value, Action> = (inout Value, Action) -> [Effect<Action>]

17:53

We will make it so that the reducer is handed an environment in addition to the current value and action, which means introducing another generic: public typealias Reducer<Value, Action, Environment> = (inout Value, Action) -> [Effect<Action>]

17:57

One way we can make use of this generic is to have the reducer curried, so that it first is given an environment, and then it will return a traditional reducer signature: public typealias Reducer<Value, Action, Environment> = (Environment) -> (inout Value, Action) -> [Effect<Action>]

18:07

This is perfectly valid, and is in fact how we approached controlling side effects in our 2nd episode of Point-Free. In that episode we curried a date value into our function so that we could control it.

18:17

However, using this signature for reducers is going to mean always returning a function from our reducer, which will get annoying after awhile. Thankfully, there’s an equivalent formulation of this in which we simply pass in the environment alongside the state and action: public typealias Reducer<Value, Action, Environment> = (inout Value, Action, Environment) -> [Effect<Action>]

18:41

We could even have curried the function in the other direction: public typealias Reducer<Value, Action, Environment> = (inout Value, Action) -> (Environment) -> [Effect<Action>]

18:52

We won’t do that, but it’s also a valid way of defining reducers, and we have exercises to explore this.

18:58

This Environment generic gives us the opportunity to provide any dependencies our reducer needs in order to produce the effects to power its logic.

19:07

This is of course going to break a lot of things, so let’s start fixing. Let’s first concentrate on only the ComposableArchitecture module.

19:09

The first compiler error we see is in the combine function, which takes a bunch of reducers of the same type and combines them into one mega-reducer. To fix this we just need to introduce the environment generic and pass it along to each of the sub-reducers: public func combine<Value, Action, Environment>( _ reducers: Reducer<Value, Action, Environment>... ) -> Reducer<Value, Action, Environment> { return { value, action, environment in let effects = reducers.flatMap { $0(&value, action, environment) } return effects } }

19:49

Simple enough. Next we have pullback , which is having troubles right now because again we need to account for the new environment generic. The simplest fix for this is to introduce an Environment generic and pass along the environment to the local reducer in the implementation of pullback : public func pullback< LocalValue, GlobalValue, LocalAction, GlobalAction, Environment >( _ reducer: @escaping Reducer<LocalValue, LocalAction, Environment>, value: WritableKeyPath<GlobalValue, LocalValue>, action: CasePath<GlobalAction, LocalAction> ) -> Reducer<GlobalValue, GlobalAction, Environment> { return { globalValue, globalAction, environment in guard let localAction = action.extract(globalAction) else { return [] } let localEffects = reducer( &globalValue[keyPath: value], localAction, environment ) return localEffects.map { localEffect in localEffect.map(action.embed) .eraseToEffect() } } }

20:17

That gets things building, but it isn’t ideal. The pullback operation on reducers was all about being able to transform reducers that work on local domains into ones that work on global domains. This was handy for modularizing, because it allowed us to split our features into modules that only contain domain types that the feature actually cares about, and then the app target could piece together those domains into the full app domain.

20:43

In order to maintain this modularity and isolate a more local environment from the unrelated details of a more global one, we want to be able to transform the environment, as well.

20:53

So, let’s introduce generics for local and global environments, and see what shape the environment transformation needs to be: public func pullback< LocalValue, GlobalValue, LocalAction, GlobalAction, LocalEnvironment, GlobalEnvironment >( _ reducer: @escaping Reducer< LocalValue, LocalAction, LocalEnvironment >, value: WritableKeyPath<GlobalValue, LocalValue>, action: CasePath<GlobalAction, LocalAction>, environment: <#???#> ) -> Reducer<GlobalValue, GlobalAction, GlobalEnvironment> {

21:21

In the body of this function we need to return a new reducer that works with global environments: return { globalValue, globalAction, globalEnvironment in

21:33

Then, when we try to run our local reducer, we need to provide it a local environment. Seems like we need a way to transform the global environment we have at hand into a local environment, and that right there defines the shape of transformation that needs to be handed to pullback : environment: @escaping (GlobalEnvironment) -> LocalEnvironment

21:44

Then we can fix our pullback by simply doing: return { globalValue, globalAction, globalEnvironment in guard let localAction = action.extract(globalAction) else { return [] } let localEffects = reducer( &globalValue[keyPath: value], localAction, environment(globalEnvironment) ) return localEffects.map { localEffect in localEffect.map(action.embed) .eraseToEffect() } }

21:54

And just like that pullback is now compiling. A few things about this:

22:00

Notice that the direction of the environment transformation is the same as it is for the value and action transformations. All three go from the global thing down to the local thing. So this transformation is still contravariant in each of its generics, and this is why pullback is still a good name for this operation.

22:23

Also we just want to say that it’s totally ok that we pullback environments along plain functions instead of key paths or case paths. The key paths and case paths are only necessary because we need a more powerful tool for picking apart state and actions and glueing them back together. But for the environment we just need to project the local environment out of the global, and a simple function does that just fine.

22:45

OK, our next compiler error in this module is the logging higher-order reducer that allows us to add logging capabilities to any reducer. We just gotta make sure to pass along the environment to the reducer that we are adding logging: public func logging<Value, Action, Environment>( _ reducer: @escaping Reducer<Value, Action, Environment> ) -> Reducer<Value, Action, Environment> { return { value, action, environment in let effects = reducer(&value, action, environment) let newValue = value return [ .fireAndForget { print("Action: \(action)") print("Value:") dump(newValue) print("---") } ] + effects } }

23:06

We could even improve this higher-order reducer now by requiring the caller to provide an environment that holds a print function. We have exercises for the viewer to explore this possibility.

23:16

And just like that, we’ve updated reducers and their transformation functions to work with the environment, and it was very simple to do. Environment in the store

23:34

But we have a bunch of errors in the Store type.

24:02

The first has to do with the fact that the Reducer it holds now expects 3 generics: private let reducer: Reducer<Value, Action> Generic type ‘Reducer’ specialized with too few type parameters (got 2, but expected 3)

24:10

We need to provide this generic somehow. It seems there isn’t much else we can do but add the generic to our store: public final class Store<Value, Action, Environment>: ObservableObject { private let reducer: Reducer<Value, Action, Environment>

24:18

That might be reasonable enough to do, after all we seem to have parity of Reducer ’s shape with Store ’s shape.

24:27

The next error is in the Store ’s initializer, which just needs the new generic: public init( initialValue: Value, reducer: @escaping Reducer<Value, Action, Environment> ) {

24:35

Next we have an error in the send method, because we are trying to invoke the reducer without providing an environment: public func send(_ action: Action) { let effects = self.reducer(&self.value, action) Missing argument for parameter #3 in call

24:48

Where are we going to get this environment from? It seems like maybe the environment should be provided to the store when we create it. That way it can hold onto the environment and pass it down to its reducer whenever an action comes in. So let’s do that: public final class Store<Value, Action, Environment>: ObservableObject { private let reducer: Reducer<Value, Action, Environment> private let environment: Environment @Published public private(set) var value: Value private var viewCancellable: Cancellable? private var effectCancellables: Set<AnyCancellable> = [] public init( initialValue: Value, reducer: @escaping Reducer<Value, Action, Environment>, environment: Environment ) { self.reducer = reducer self.value = initialValue self.environment = environment }

25:11

And now we have something to pass along to our reducer: public func send(_ action: Action) { let effects = self.reducer(&self.value, action, self.environment)

25:15

So far these errors have been pretty straightforward to fix.

25:19

But the next one is a little trickier. It’s in the view method on Store , which allows us transform our store into one that exposes only a local domain. This is how we are able to take the store in one SwiftUI view and pass it on to a smaller view that needs only a part of the state and actions of the parent.

25:53

Right now it is complaining because we are missing the 3rd generic: public func view<LocalValue, LocalAction>( value toLocalValue: @escaping (Value) -> LocalValue, action toGlobalAction: @escaping (LocalAction) -> Action ) -> Store<LocalValue, LocalAction> { let localStore = Store<LocalValue, LocalAction>( Generic type ‘Store’ specialized with too few type parameters (got 2, but expected 3) Since this function is all about transforming a store on a global domain into one on a local domain, we would probably think we are supposed to provide another transformation alongside the value and action transformations we have here.

26:00

So, let’s start by introducing a generic for the local environment, and a new transformation argument: public func view<LocalValue, LocalAction, LocalEnvironment>( value toLocalValue: @escaping (Value) -> LocalValue, action toGlobalAction: @escaping (LocalAction) -> Action, environment: <#???#> ) -> Store<LocalValue, LocalAction, LocalEnvironment> { let localStore = Store<LocalValue, LocalAction, LocalEnvironment>( … environment: <#???#> )

26:29

If we want to provide that argument, we’ll see that we need to provide a local environment and we have a global environment at hand, so that seems to determine what direction our transformation should go: public func view<LocalValue, LocalAction, LocalEnvironment>( value toLocalValue: @escaping (Value) -> LocalValue, action toGlobalAction: @escaping (LocalAction) -> Action, environment toLocalEnvironment: @escaping (Environment) -> LocalEnvironment ) -> Store<LocalValue, LocalAction, LocalEnvironment> { let localStore = Store<LocalValue, LocalAction, LocalEnvironment>( initialValue: toLocalValue(self.value), reducer: { localValue, localAction in self.send(toGlobalAction(localAction)) localValue = toLocalValue(self.value) return [] }, environment: toLocalEnvironment(self.environment) )

27:06

We’re getting closer, we just have one more more compiler error in this method: Contextual closure type ‘(inout LocalValue, LocalAction, LocalEnvironment) -> [Effect<LocalAction>]’ expects 3 arguments, but 2 were used in closure body The reducer we have implemented here is using the old signature of reducers, with just two arguments. We now have a 3rd argument, the environment, which is the local environment here: reducer: { localValue, localAction, localEnvironment in

27:16

And that strangely gets things compiling. We weirdly don’t even need to use the local environment. We could in fact use an underscore to show that it isn’t being used at all: reducer: { localValue, localAction, _ in

27:31

This means we aren’t really using the environment in any meaningful way. All we are doing is transforming it pass it on to the new store, but the new store doesn’t use it all. Erasing the environment from the store

27:44

The ComposableArchitecture module is finally building, but we encountered something strange along the way. The implementation of the view method on Store is very strange, and makes me question whether or not we are doing things right. We needed to account for the environment in the view method, even though it wasn’t used in any meaningful way.

28:05

In fact, the store as an entire concept has very little to do with the environment. Users of a Store only care about getting state values out of it and sending actions to it. They never access the environment or even need to know about the environment that is being used under the hood.

28:30

We’d like to “erase” this type from the Store class. That is, we want to remove the generic from Store , and hide the environment detail inside the implementation of the class rather than exposing it to the public.

28:46

There’s a lot that can be said about type erasing in Swift, but we are fortunate enough that erasing this type is very easy to do for the Store type. Let’s start by removing the environment generic so that we can see what it is we need to do to fix this: public final class Store<Value, Action>: ObservableObject {

29:04

With the generic gone we can no longer hold onto a reducer or environment with the Environment type specified. However, we could replace those types with Any so that we don’t need a concrete type to put in now, but any value could be used at runtime: public final class Store<Value, Action>: ObservableObject { private let reducer: Reducer<Value, Action, Any> private let environment: Any

29:19

Then, for the initializer, which is a place where we actually do need environment information, we can introduce a generic and use it for the reducer and environment arguments: public init<Environment>( initialValue: Value, reducer: @escaping Reducer<Value, Action, Environment>, environment: Environment ) { self.reducer = reducer self.value = initialValue self.environment = environment }

29:33

This introduces a compiler error because it doesn’t know how to translate between a reducer with an Any environment and a reducer with a specific environment: Cannot assign value of type ‘(inout Value, Action, Environment) -> [Effect<Action>]’ to type ‘(inout Value, Action, Any) -> [Effect<Action>]’

29:42

We can fix this by implementing a custom reducer right in this initializer that force casts the Any value to a proper Environment value. This may sound dangerous, but if we are careful we will never accidentally get an environment we don’t expect. So let’s try it out: public init<Environment>( initialValue: Value, reducer: @escaping Reducer<Value, Action, Environment>, environment: Environment ) { self.reducer = { value, action, environment in reducer(&value, action, environment as! Environment) } self.value = initialValue self.environment = environment }

30:11

That gets the initializer compiling, but the view method is having problems again, but all we have to do is remove the environment generics and pass along our own environment to the new store: 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: self.environment ) localStore.viewCancellable = self.$value .sink { [weak localStore] newValue in localStore?.value = toLocalValue(newValue) } return localStore }

30:35

And things are compiling! The force cast may seem scary, but it’s also possible to do this type erasure without doing a force cast. It’s a little more complicated than what we have shown here, so we will leave it as an exercise for the viewer.

31:20

It also may seem weird that we didn’t need to transform the environment explicitly in view , but remember that the environment is purely an implementation detail inside the store, and all of the environment transformations have already been applied to the reducers. Till next time

31:58

And the ComposableArchitecture is now building again, but we have greatly simplified the API of the Store type.

32:24

Now let’s get the rest of the app building.

32:32

Luckily the app is fully modularized, and so we can start with our simplest modules that have the fewest dependencies, and work our way back to the main app target. Also lucky for us is that we are already using the environment technique in our code, and that will make converting to this new style very easy…next time! References Dependency Injection Made Easy Brandon Williams & Stephen Celis • May 21, 2018 This is the episode that first introduced our Current environment approach to dependency injection. Note 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. https://www.pointfree.co/episodes/ep16-dependency-injection-made-easy Dependency Injection Made Comfortable Brandon Williams & Stephen Celis • Jun 4, 2018 Note Let’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. https://www.pointfree.co/episodes/ep18-dependency-injection-made-comfortable How to Control the World Stephen Celis • Sep 24, 2018 Stephen 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. https://vimeo.com/291588126 Effectful State Management: Synchronous Effects Brandon Williams & Stephen Celis • Oct 14, 2019 This 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. Note 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. https://www.pointfree.co/episodes/ep76-effectful-state-management-synchronous-effects Testable State Management: Reducers Brandon Williams & Stephen Celis • Nov 25, 2019 This 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. https://www.pointfree.co/episodes/ep82-testable-state-management-reducers Downloads Sample code 0091-modular-dependency-injection-pt1 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 .