EP 77 · Effectful State Management · Oct 21, 2019 ·Members

Video #77: Effectful State Management: Unidirectional Effects

smart_display

Loading stream…

Video #77: Effectful State Management: Unidirectional Effects

Episode: Video #77 Date: Oct 21, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep77-effectful-state-management-unidirectional-effects

Episode thumbnail

Description

We’ve modeled side effects in our architecture, but it’s not quite right yet: a reducer can write to the outside world, but it can’t read data back in! This week our architecture’s dedication to unidirectional data flow will lead us there.

Video

Cloudflare Stream video ID: 2f65e08b627b92592135855227fb350e Local file: video_77_effectful-state-management-unidirectional-effects.mp4 *(download with --video 77)*

References

Transcript

0:05

We have extracted our first effect into our architecture. Let’s reflect on what we just accomplished. Recap We had a side effect in our view, saving favorite primes to disk, that we knew we needed some way to control. For one thing it’s untestable code, but for another we have found that a useful way to simplify our views was to move all of their logic into our reducers, and simply make the view responsible for sending user actions to the store.

0:18

So, we naively and literally moved the side effecting code into the reducer. This technically got the job done, but that destroyed all of the nice testability and understandability of the reducer.

0:28

So, we recalled some of the lessons we learned from our previous episode on side effects , in particular we can often push effects to the boundary of a function by introducing a new output to the function that represents the effect we want to execute without actually executing it.

0:42

This led us to change our reducer signature to be a function that returns a void-to-void closure, which can hold the side effecting work without actually executing it, which passes the responsibility of executing the work to the store.

0:54

After fixing some compiler errors we were able to finally encapsulate the saving work in a closure, and get the store to run it for us rather than doing it in the reducer. Best of all, changing the reducer signature like this didn’t prevent us from still having composable reducers and stores, which is the true power behind this type of architecture.

1:03

This is our first step to introducing effects to our architecture, and it was a simple effect. We have a few more steps to go because there are more complicated effects we need to model. For example, loading our list of favorite primes is a little different from saving. Saving is mostly a fire-and-forget operation, we just run the work and we don’t need to communicate back to the reducer that anything happened.

1:24

However, loading does some work to load data from the disk, and then somehow we want to communicate that work back to the reducer.

1:35

So let’s figure out how we need to change this effects model in order to capture that ability. Synchronous effects that produce results

1:45

We now want to move this loading side effect into our reducer. Button("Load") { let documentsPath = NSSearchPathForDirectoriesInDomains( .documentDirectory, .userDomainMask, true )[0] let documentsUrl = URL(fileURLWithPath: documentsPath) let favoritePrimesUrl = documentsUrl .appendingPathComponent("favorite-primes.json") guard let data = try? Data(contentsOf: favoritePrimesUrl), let favoritePrimes = try? JSONDecoder() .decode([Int].self, from: data) else { return } self.store.send(.loadedFavoritePrimes(favoritePrimes)) }

2:00

We already have an action that is sent to the store when the load button is tapped, but it’s not quite right. The view is doing the side effect work to get the array of favorite primes, and then sending that data off to the store in the action. We’d like to move that side effect to the reducer, so let’s change the the load button to, instead, send an action to the store. Button("Load") { self.store.send(.loadButtonTapped)

2:28

In order for this to work, we need to introduce a new action to our action enum: public enum FavoritePrimesAction { … case loadButtonTapped

2:37

And we need to handle this case in our reducer: case .loadButtonTapped:

2:47

But what do we do here? Well, we want to do all that work we were previously doing in the view. case .loadButtonTapped: let documentsPath = NSSearchPathForDirectoriesInDomains( .documentDirectory, .userDomainMask, true )[0] let documentsUrl = URL(fileURLWithPath: documentsPath) let favoritePrimesUrl = documentsUrl .appendingPathComponent("favorite-primes.json") guard let data = try? Data(contentsOf: favoritePrimesUrl), let favoritePrimes = try? JSONDecoder() .decode([Int].self, from: data) else { return } self.store.send(.favoritePrimesLoaded(favoritePrimes))

2:55

We don’t want to do this work directly, though. We want to wrap it up in an effect closure so that the reducer doesn’t perform the side effect, the store does. case .loadButtonTapped: return { let documentsPath = NSSearchPathForDirectoriesInDomains( .documentDirectory, .userDomainMask, true )[0] let documentsUrl = URL(fileURLWithPath: documentsPath) let favoritePrimesUrl = documentsUrl .appendingPathComponent("favorite-primes.json") guard let data = try? Data(contentsOf: favoritePrimesUrl), let favoritePrimes = try? JSONDecoder() .decode([Int].self, from: data) else { return } self.store.send(.favoritePrimesLoaded(favoritePrimes)) } Use of unresolved identifier ‘self’

3:12

But even this isn’t quite right. We want to send the result of all this work to the store, but in the reducer, we have no self or store available to us.

3:27

When this logic was in the view it ran the effect and sent an action to the store to let it know that state should be mutated. Now that we’ve moved the effect into our reducer we need to rethink things, because we need to provide a way of taking the result of an effect and feeding it right back into the reducer!

3:51

Sadly, this means the current way we model effects, as a () -> Void closure, isn’t quite right. It needs a little more in order to be able to make changes to the reducer’s future state. We already have an action, favoritePrimesLoaded , that does just that, so it sounds like instead of firing effects off into the Void , we need to give them the ability to return an action that can influence state.

4:24

This means that maybe our effect type should have the following signature: public typealias Effect<Action> = () -> Action

4:50

This means that the work happening in the Effect closure can be purely interested in only doing the bare minimum of work needed to get the effect done, and then send the result of the work back to the reducer by wrapping it in an action.

5:06

But this isn’t quite the shape we want because not all effects produce data that needs to be fed back into the system. For example, our existing effect that writes favorite primes to disk. Maybe instead what we want is for the effect to return an optional action, so that effects that are fire-and-forget can just return nil , and those will be ignored by the store. public typealias Effect<Action> = () -> Action?

5:33

We can update our reducer signature accordingly. public typealias Reducer<Value, Action> = (inout Value, Action) -> Effect Reference to generic type ‘Effect’ requires arguments in <…> By supplying the Action generic. public typealias Reducer<Value, Action> = (inout Value, Action) -> Effect<Action>

5:44

The send function once again needs to be updated. public func send(_ action: Action) { let effect = self.reducer(&self.value, action) effect() Result of call to function returning ‘Action?’ is unused

5:48

This is a good warning to have. Calling the effect now returns a value we need to handle, so we should be handling it public func send(_ action: Action) { let effect = self.reducer(&self.value, action) let action = effect()

6:04

It’s an optional action, so we can first try to unwrap it. public func send(_ action: Action) { let effect = self.reducer(&self.value, action) if let action = effect() { }

6:12

And we can feed this action right back to send . public func send(_ action: Action) { let effect = self.reducer(&self.value, action) if let action = effect() { self.send(action) }

6:17

And this is a kind of feedback cycle we get: when an effect is run, if it produces an action, we can feed it directly back to the store.

6:25

The view method doesn’t compile yet. The no-op closure must now explicitly return a nil action. return { nil }

6:45

Our reducer composition functions need to be updated to work with actionable effects instead. First: combine . public func combine<Value, Action>( _ reducers: Reducer<Value, Action>... ) -> Reducer<Value, Action> { return { value, action in let effects = reducers.map { $0(&value, action) } return { for effect in effects { effect() } } Result of call to function returning ‘Action?’ is unused

6:46

This is the same warning as before, and it’s a good one, because it’s letting us know that we’re not handling the action an effect may produce. return { for effect in effects { let action = effect() } }

6:59

Let’s remember that what we are returning here is a () -> Action? closure. return { () -> Action? in

7:08

So somewhere in this closure we need to return an optional action.

7:12

Technically, we already have one, so we could return it right away. return { () -> Action? in for effect in effects { let action = effect() return action } }

7:16

This isn’t what we want because it means we’ll only ever execute the first effect, and only its optional action can be fed back into the reducer. This means any other effects produced later on would be ignored.

7:33

We could instead run every effect and track the final, non-nil action produced. return { () -> Action? in var finalAction: Action? for effect in effects { let action = effect() if let action = action { finalAction = action } } return finalAction }

7:55

But this is also strange, because even though we are running every effect, we’re only feeding the last action an array of effects produces, which means we could potentially miss out on some actions that should be fed back to the store. Combining multiple effects that produce results

8:16

It seems like the shape of our reducer function is still not quite right. public typealias Reducer<Value, Action> = (inout Value, Action) -> Effect<Action>

8:24

It’s no longer enough to return a single effect of an action because when we combine our reducers together, we’re forced to lose some information.

8:38

If instead, our reducers could return arrays of effects, we could address this problem. public typealias Reducer<Value, Action> = (inout Value, Action) -> [Effect<Action>]

8:46

This breaks send because we must now loop over each effect and unwrap it before feeding it back into the send method. public func send(_ action: Action) { let effects = self.reducer(&self.value, action) effects.forEach { effect in if let action = effect() { self.send(action) } } }

9:20

In the view method we can now return an empty array instead of a no-op closure of nil . return []

9:33

We should now be able to redefine the combine function. Effects are now returned as arrays, so the result of mapping over our reducers and collecting all of their effects has led to nested arrays of effects. public func combine<Value, Action>( _ reducers: Reducer<Value, Action>... ) -> Reducer<Value, Action> { return { value, action in let effects = reducers.map { $0(&value, action) } // [[() -> Action?]]

9:51

Luckily, there’s a tool for this job that we’ve explored in depth on Point-Free (The Many Faces of Flat‑Map: part 1 , part 2 , part 3 , part 4 , part 5 ). Whenever you encounter this nesting problem, where map ping a value further nests it, we could turn to flatMap ! So by updating map to flatMap we can map our reducers into arrays of effects and then flatten them into shallow array of effects. let effects = reducers.flatMap { $0(&value, action) }

10:10

And this is exactly what we want to return here. return effects Pulling local effects back globally

10:15

What about pullback ? public func pullback< LocalValue, GlobalValue, LocalAction, GlobalAction >( _ reducer: @escaping Reducer<LocalValue, LocalAction>, value: WritableKeyPath<GlobalValue, LocalValue>, action: WritableKeyPath<GlobalAction, LocalAction?> ) -> Reducer<GlobalValue, GlobalAction> { return { globalValue, globalAction in guard let localAction = globalAction[keyPath: action] else { return {} } Cannot convert return expression of type ‘() -> ()’ to return type ‘[() -> GlobalAction]’

10:18

Here we can swap out our empty closure for an empty array. guard let localAction = globalAction[keyPath: action] else { return [] }

10:26

For the second error, we must deal with the fact that effects now have the ability to return actions, and here we’re working with both local actions and global actions. let effect = reducer(&globalValue[keyPath: value], localAction) return effect Cannot convert return expression of type ‘[() -> LocalAction]’ to return type ‘[() -> GlobalAction]’

10:39

We can rename effect to localEffects because it’s now an array of local effects that return local actions. let localEffects = reducer( &globalValue[keyPath: value], localAction )

10:43

And we need to somehow transform this array of local effects that return local actions into an array of global effects that return global actions. Let’s try with map . Every time the block runs, it’s passed a local effect. localEffects.map { localEffect in }

10:52

And we know we need to return a global effect, so we can return a brand new closure describing that effect. localEffects.map { localEffect in return { () -> GlobalAction? in } }

11:03

But how do we produce a global effect? We need this closure to return an optional global action, ideally from an optional local action. We can get an optional local action by evaluating the local effect. localEffects.map { localEffect in return { () -> GlobalAction? in let localAction = localEffect() } }

11:16

If we can’t unwrap it, we can return a nil global action. localEffects.map { localEffect in return { () -> GlobalAction? in guard let localAction = localEffect() else { return nil } } }

11:30

After this guard we have an honest local action, but the closure we are in needs to have a global action returned. How can we do this?

11:39

Well, luckily we have a writable key path that can do just this! In order to get a mutable global action, we can make a copy. localEffects.map { localEffect in return { () -> GlobalAction? in guard let localAction = localEffect() else { return nil } var globalAction = globalAction } }

11:53

From here, we can embed a local action using the key path subscript. localEffects.map { localEffect in return { () -> GlobalAction? in guard let localAction = localEffect() else { return nil } var globalAction = globalAction globalAction[keyPath: action] = localAction } }

12:05

And finally, we can return the global action from the global effect. localEffects.map { localEffect in return { () -> GlobalAction? in guard let localAction = localEffect() else { return nil } var globalAction = globalAction globalAction[keyPath: action] = localAction return globalAction } }

12:08

And we can finally return the result. return localEffects.map { localEffect in

12:12

Alright, the compiler is happy with pullback . Let’s take a moment to explain what is going on because this function is doing quite a bit.

12:23

It’s pretty awesome that the enum properties we generated for our action enum come with setters so that we get writable key paths from them, which is exactly what we needed to implement this pullback.

12:47

The pullback function is our way of turning reducers that work on local states and actions into reducers that work on global states and actions. It does this by constructing a global reducer such that when a global state and action comes it, it uses the key paths to extract out local state and action, runs the local reducer on it, and then uses the key paths to plug that new local state back into the global state.

13:29

In addition to this, running the local reducer now produces an array of local effects, that is effects which can send local actions back into the system. We can convert a local effect into a global effect by running the local effect, getting the resulting local action it produced, and using the writable key path to embed it into the global actions. Applying this conversion to every effect in the array of local effects completes the implementation of this function.

13:57

pullback is doing quite a bit, but it’s all very natural, mechanical work. We don’t have a lot of choice of other things to do when trying to implement this function. It’s basically always a process of converting local things to global things with the key paths.

14:19

We’ve got one more thing to update in this file, and it’s the logging higher-order reducer. public func logging<Value, Action>( _ reducer: @escaping Reducer<Value, Action> ) -> Reducer<Value, Action> { return { value, action in let effect = reducer(&value, action) let newValue = value return { Cannot convert return expression of type ‘() -> ()’ to return type ‘[() -> Action?]’

14:22

We need to make a few changes here. The reducer now returns an array of effects. return { value, action in let effects = reducer(&value, action)

14:27

And we need to prepend these effects with our logging effect. return { value, action in let effects = reducer(&value, action) let newValue = value return [ { print("Action: \(action)") print("Value:") dump(newValue) print("---") return nil } ] + effects } Working with our new effects

14:48

Alright! the Composable Architecture is now fully building, but the favorite primes module is not.

14:55

First we can update the signature to return an array of effects generic over its action type. public func favoritePrimesReducer( state: inout [Int], action: FavoritePrimesAction ) -> [Effect<FavoritePrimesAction>] {

15:11

And now we have a side effects we need to fix. We can update the no-op closures to no-op arrays. case let .deleteFavoritePrimes(indexSet): for index in indexSet { state.remove(at: index) } return [] case let .loadedFavoritePrimes(favoritePrimes): state = favoritePrimes return []

15:19

The saveButtonTapped effect needs to be wrapped in an array and return nil to signify it’s a fire-and-forget action that will not feed any result back to the store. case .saveButtonTapped: let state = state return [{ let data = try! JSONEncoder().encode(state) let documentsPath = NSSearchPathForDirectoriesInDomains( .documentDirectory, .userDomainMask, true )[0] let documentsUrl = URL(fileURLWithPath: documentsPath) let favoritePrimesUrl = documentsUrl .appendingPathComponent("favorite-primes.json") try! data.write(to: favoritePrimesUrl) return nil }]

15:35

And loadButtonTapped is a similar story. We can start by wrapping it in an array: case .loadButtonTapped: return [{ let documentsPath = NSSearchPathForDirectoriesInDomains( .documentDirectory, .userDomainMask, true )[0] let documentsUrl = URL(fileURLWithPath: documentsPath) let favoritePrimesUrl = documentsUrl .appendingPathComponent("favorite-primes.json") guard let data = try? Data(contentsOf: favoritePrimesUrl), let favoritePrimes = try? JSONDecoder() .decode([Int].self, from: data) else { return } self.store.send(.favoritePrimesLoaded(favoritePrimes)) }]

15:43

Where we previously relied on access to the store, we can now return this action directly from the effect so that the store can evaluate it later. return .favoritePrimesLoaded(favoritePrimes)

15:56

And finally, we now need to return nil where we were previously bailing out early. else { return nil }

16:06

The module’s now in building order, so we should be able to run our playground, where everything works exactly as it did before, but now all of the side effects are executed by the store, and our reducer remains a pure function.

16:25

One bummer about moving this effect work into the reducer is that our reducer has gotten quite big.

16:35

As we support more actions and more effects we run the risk of reducers getting huge and difficult to read. One common way to fix that is to extract this effects out to little private helpers so that our reducer can stay nice and succinct. For example, we can extract the save effect to a private function. private func saveEffect( favoritePrimes: [Int] ) -> Effect<FavoritePrimesAction> { return { let data = try! JSONEncoder().encode(favoritePrimes) let documentsPath = NSSearchPathForDirectoriesInDomains( .documentDirectory, .userDomainMask, true )[0] let documentsUrl = URL(fileURLWithPath: documentsPath) try! data.write( to: documentsUrl.appendingPathComponent("favorite-primes.json") ) return nil } }

17:12

And we can call out to it from our reducer. case .saveButtonTapped: let state = state return [saveEffect(favoritePrimes: state)]

17:31

We also have the benefit that we can remove the let state = state dance needed to access state in the effect closure. This was previously required because state is mutable, and you are not allowed to access that from an escaping closure. But in our saveEffect function we are able to make it explicit that we want an immutable array of integers, and so now we can pass state directly to it. case .saveButtonTapped: return [saveEffect(favoritePrimes: state)]

17:49

The load effect can be similarly extracted: private let loadEffect: Effect<FavoritePrimesAction> = { let documentsPath = NSSearchPathForDirectoriesInDomains( .documentDirectory, .userDomainMask, true )[0] let documentsUrl = URL(fileURLWithPath: documentsPath) let favoritePrimesUrl = documentsUrl .appendingPathComponent("favorite-primes.json") guard let data = try? Data(contentsOf: favoritePrimesUrl), let favoritePrimes = try? JSONDecoder() .decode([Int].self, from: data) else { return nil } return .favoritePrimesLoaded(favoritePrimes) }

18:08

And called out to in the reducer: case .loadButtonTapped: return [loadEffect]

18:15

By extracting these effects we’ve made our reducer a lot more succinct. And it’s always very easy to refactor complex, inline effects into extracted helpers. What’s unidirectional data flow?

18:25

So, one more effect has been extracted. Let’s again take a moment to reflect on what we have accomplished.

18:32

We wanted to extract the loading from disk effect out of our view and somehow model it in the reducer. We quickly realized that this effect was not quite like the previous effect we handled. The save effect was essentially fire-and-forget, it just did its work and didn’t need to notify anyone of anything after.

18:41

However, the loading effect needed to somehow feed its loaded data back into the reducer so that we could react. This led us to refactoring the effecting signature from being a void-to-void closure to being a void-to-optional action closure. This allows effects to do the bare minimum of work necessary to get the job done, and then feed the result back into the reducer by sending another action. Then the store becomes the interpreter of these effects by first running the reducer, collecting all of the effects that want to be executed, iterating over that error to execute the effects, and then sending any actions the effects produced back into the store.

19:22

This right here is what people refer to when they say “unidirectional data flow.” Data is only ever mutated in one single way: an action comes into the reducer which allows the reducer to mutate the state. If you want to mutate the state via some side effect work, you have no choice but to construct a new action that can then be fed back into the reducer, which only then gives you the ability to mutate.

19:58

This kind of data flow is super understandable because you only have one place to look for how state can be mutated, but it also comes at the cost of needing to add extra actions to take care of feeding effect results back into the reducer. This is why many UI frameworks, SwiftUI included, give ways to sidestep the strict unidirectional style in order to simplify usage, as they do with two-way bindings, but this can be at the cost of complicating how data flows through the UI. Next time: asynchronous effects

20:37

Two effects down, one to go, and this last one isn’t a simple, synchronous effect, as were the last two. Synchronous effects are going to cause some problems down the road, because they can completely block the application. So let’s try to capture that last effect in our architecture and see what happens…next time! References Elm: Commands and Subscriptions Elm is a pure functional language wherein applications are described exclusively with unidirectional data flow. It also has a story for side effects that closely matches the approach we take in these episodes. This document describes how commands (like our effect functions) allow for communication with the outside world, and how the results can be mapped into an action (what Elm calls a “message”) in order to be fed back to the reducer. https://guide.elm-lang.org/effects/ Redux: Data Flow The Redux documentation describes and motivates its “strict unidirectional data flow.” https://redux.js.org/basics/data-flow Redux Middleware Redux, at its core, is very simple and has no single, strong opinion on how to handle side effects. It does, however, provide a means of layering what it calls “middleware” over reducers, and this third-party extension point allows folks to adopt a variety of solutions to the side effect problem. https://redux.js.org/advanced/middleware Redux Thunk Redux Thunk is the recommended middleware for basic Redux side effects logic. Side effects are captured in “thunks” (closures) to be executed by the store. Thunks may optionally utilize a callback argument that can feed actions back to the store at a later time. https://github.com/reduxjs/redux-thunk ReSwift ReSwift is one of the earliest, most popular Redux-inspired libraries for Swift. Its design matches Redux, including its adoption of “middleware” as the primary means of introducing side effects into a reducer. https://github.com/ReSwift/ReSwift SwiftUIFlux Thomas Ricouard An early example of Redux in SwiftUI. Like ReSwift, it uses “middleware” to handle side effects. https://github.com/Dimillian/SwiftUIFlux Elm: A delightful language for reliable webapps Elm is both a pure functional language and framework for creating web applications in a declarative fashion. It was instrumental in pushing functional programming ideas into the mainstream, and demonstrating how an application could be represented by a simple pure function from state and actions to state. https://elm-lang.org Redux: A predictable state container for JavaScript apps. The idea of modeling an application’s architecture on simple reducer functions was popularized by Redux, a state management library for React, which in turn took a lot of inspiration from Elm . https://redux.js.org Composable Reducers Brandon Williams • Oct 10, 2017 A talk that Brandon gave at the 2017 Functional Swift conference in Berlin. The talk contains a brief account of many of the ideas covered in our series of episodes on “Composable State Management”. https://www.youtube.com/watch?v=QOIigosUNGU Downloads Sample code 0077-effectful-state-management-unidirectional-effects 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 .