EP 78 · Effectful State Management · Oct 28, 2019 ·Members

Video #78: Effectful State Management: Asynchronous Effects

smart_display

Loading stream…

Video #78: Effectful State Management: Asynchronous Effects

Episode: Video #78 Date: Oct 28, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep78-effectful-state-management-asynchronous-effects

Episode thumbnail

Description

It’s time to finish our architecture’s story for side effects. We’ve described synchronous effects and unidirectional effects, but we still haven’t captured the complexity of async effects. Let’s fix that with a final, functional refactor.

Video

Cloudflare Stream video ID: 27962a75a5d2fb4bbf278a180576ebe9 Local file: video_78_effectful-state-management-asynchronous-effects.mp4 *(download with --video 78)*

References

Transcript

0:05

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

0:12

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.

0:21

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.

1:12

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.

1:48

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. Extracting our asynchronous effect

2:17

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.

2:30

However, this model of effects still has room for improvement. Currently it only supports synchronous effects, because an effect needs to be able to return an optional action immediately. It has no ability to do some work that can take time, and then later return an action. This means that if an effect needs time to do its work, it will block the rest of the effects from running, and will even block any new events from being sent to the store.

2:44

This is exactly what we need to be able to support if we want to handle our most complicated effect in the application: a network request that sends the current count to Wolfram Alpha to find the nth prime. How can we move this effect into our architecture?

2:58

We’ll be working with the counter screen now, which currently performs our asynchronous side effect.

3:14

Unfortunately, the counter module is not quite in working order right now because we haven’t updated it to support the changes we’ve made to our architecture, so let’s update it to do just that by introducing effects to each reducer.

3:39

We can start with the counter reducer by updating its signature to return an array of effects. public func counterReducer( state: inout Int, action: CounterAction ) -> [Effect<CounterAction>] {

3:50

And then we return an empty array from each action handled, since neither incrementing nor decrementing the count should fire off an effect. case .decrTapped: state -= 1 return [] case .incrTapped: state += 1 return []

3:58

Next, we need to update the prime modal reducer with pretty much the same changes, starting with its signature: public func primeModalReducer( state: inout PrimeModalState, action: PrimeModalAction ) -> [Effect<PrimeModalAction>] {

4:11

And then returning empty arrays of effects. case .removeFavoritePrimeTapped: state.favoritePrimes.removeAll(where: { $0 == state.count }) return [] case .saveFavoritePrimeTapped: state.favoritePrimes.append(state.count) return []

4:18

The counter module is now building again, so let’s take a look at the asynchronous effect that we’re working with: asking the Wolfram Alpha API for the “nth prime,” where “n” is the current count of our counter app.

4:45

In the counter view is a button that fires off this request. Button( "What is the \(ordinal(self.store.value.count)) prime?", action: self.nthPrimeButtonAction )

4:57

Which calls to a local method, which mutates some local view state and performs the asynchronous side effect inline. func nthPrimeButtonAction() { self.isNthPrimeButtonDisabled = true nthPrime(self.store.value.count) { prime in self.alertNthPrime = prime.map(PrimeAlert.init(prime:)) self.isNthPrimeButtonDisabled = false } } The local state involved includes disabling the button when a request is in flight, and showing an alert with the result when the request succeeds.

5:30

In order to capture this effect in our reducer, let’s follow the routine that’s worked for us so far. Instead of performing this work inline, we’d like to be able to send an action to the store so that the effect can be captured in the reducer. func nthPrimeButtonAction() { // self.isNthPrimeButtonDisabled = true // nthPrime(self.store.value.count) { prime in // self.alertNthPrime = prime.map(PrimeAlert.init(prime:)) // self.isNthPrimeButtonDisabled = false // } self.store.send(.nthPrimeButtonTapped) }

5:48

For this to work, we must add the action to our enum full of counter actions. public enum CounterAction { case decrTapped case incrTapped case nthPrimeButtonTapped }

5:58

And we need to handle this new case in our counter reducer. case .nthPrimeButtonTapped: <#code#>

6:03

In this case we want to fire off an effect. case .nthPrimeButtonTapped: return [{ }]

6:08

In particular, something that captures the work we were doing in the view. case .nthPrimeButtonTapped: return [{ self.isNthPrimeButtonDisabled = true nthPrime(self.store.value.count) { prime in self.alertNthPrime = prime.map(PrimeAlert.init(prime:)) self.isNthPrimeButtonDisabled = false } }] Use of unresolved identifier ‘self’

6:11

We no longer have access to the view’s local state and store and self . By moving to the reducer we need to account for everything in state and actions. Local state to global state

6:25

First we must introduce some additional state, including whether or not the nth prime button is disabled, as well as the alert state. We can define a type alias to a tuple to hold all this state. public typealias CounterState = ( alertNthPrime: PrimeAlert?, count: Int, isNthPrimeButtonDisabled: Bool )

7:12

And we need to make PrimeAlert public in the process. public struct PrimeAlert: Identifiable { let prime: Int public var id: Int { self.prime } }

7:18

We can now update the counter reducer to work with this additional state. public func counterReducer( state: inout CounterState, action: CounterAction ) -> [Effect<CounterAction>] {

7:24

And when we mutate the count, we must now dig deeper into the state’s count . case .decrTapped: state.count -= 1 return [] case .incrTapped: state.count += 1 return []

7:33

And for our new action, we can remove some of those self s. We can start by extracting the pure isNthPrimeButtonDisabled mutation outside of the effect. case .nthPrimeButtonTapped: state.isNthPrimeButtonDisabled = true

7:44

And order to pass the count to nthPrime , we need to first create an immutable, local copy, so that the effect closure doesn’t attempt to capture a mutable, in-out value. case .nthPrimeButtonTapped: state.isNthPrimeButtonDisabled = true let count = state.count return [{ nthPrime(count) { prime in

7:53

The rest of the work done in the effect is done after the request has completed, where we have data that we want to flow back into the system. The way to do this is to introduce a new action to handle the nth prime response. public enum CounterAction { case decrTapped case incrTapped case nthPrimeButtonTapped case nthPrimeResponse }

8:12

And nthPrimeResponse should bundle up the prime number returned by Wolfram Alpha. case nthPrimeResponse(Int?) We are using an optional Int to signify that something could go wrong when fetching the prime, like the network request could fail.

8:21

We’ve broken our reducer, so let’s switch over the additional case. case .nthPrimeResponse(_): <#code#>

8:27

And we can move the work from the nthPrime completion handler here. case .nthPrimeButtonTapped: state.isNthPrimeButtonDisabled = true let count = state.count return [{ nthPrime(count) { prime in } }] case let .nthPrimeResponse(prime): state.alertNthPrime = prime.map(PrimeAlert.init(prime:)) state.isNthPrimeButtonDisabled = false return []

8:44

But how do we make this effect work? return [{ nthPrime(count) { prime in } }] Cannot convert value of type ‘Void’ to closure result type ‘CounterAction?’ Because we receive the prime number in a callback function, we can’t merely return the action from there. return [{ nthPrime(count) { prime in return .nthPrimeResponse(prime) } }]

8:58

Somehow, we need to return the nthPrimeResponse action outside of the callback. return [{ nthPrime(count) { prime in } return .nthPrimeResponse(prime) }] Use of unresolved identifier ‘prime’

9:04

We need access to the prime here, but that’s a value that we only get access to asynchronously, while our reducer’s effect right now must be synchronous.

9:27

We can force our network request to be synchronous by introducing a dispatch semaphore, which will then give us access to the computed prime after it executes: return [{ var p: Int? let sema = DispatchSemaphore(value: 0) nthPrime(count) { prime in p = prime sema.signal() } sema.wait() return .nthPrimeResponse(p) }]

10:16

While our reducer is happy, things aren’t quite building yet, because we’ve changed the counter reducer’s state and can no longer pull it back to the module’s reducer just by a count. We now have a bunch of additional state to handle.

10:30

First, we must account for this additional state in the module’s root state, which is represented in a tuple. public typealias CounterViewState = ( alertNthPrime: PrimeAlert?, count: Int, favoritePrimes: [Int], isNthPrimeButtonDisabled: Bool )

10:50

But this isn’t going to work, because we need to provide a single key path that plucks out the fields the counter reducer and prime modal reducer care about. In order to get access to such a key path, we must define some computed properties, which means we must upgrade CounterViewState to be a struct. public struct CounterViewState { var alertNthPrime: PrimeAlert? var count: Int var favoritePrimes: [Int] var isNthPrimeButtonDisabled: Bool )

11:20

Now we can introduce a couple of computed properties that plucks out sub-state for both the counter reducer and the prime modal reducer. var counter: CounterState { } var primeModal: PrimeModalState { }

11:31

In order for this key path to be writable, which is a requirement of the pullback function, we need both getters and setters. var counter: CounterState { get {} set {} }

11:36

The getter needs to return the tuple of CounterState it cares about. var counter: CounterState { get { (self.alertNthPrime, self.count, self.isNthPrimeButtonDisabled) } set {} }

11:47

And the setter needs to destructure those values and reassign them. var counter: CounterState { get { (self.alertNthPrime, self.count, self.isNthPrimeButtonDisabled) } set { ( self.alertNthPrime, self.count, self.isNthPrimeButtonDisabled ) = newValue } }

11:52

Prime modal state is similar, but it cares about different fields. var primeModal: PrimeModalState { get { (self.count, self.favoritePrimes) } set { (self.count, self.favoritePrimes) = newValue } }

12:01

Finally, we have two brand new key paths that will help us compose the module’s root reducer. public let counterViewReducer = combine( pullback( counterReducer, value: \CounterViewState.counter, action: \CounterViewAction.counter ), pullback( primeModalReducer, value: \.primeModal, action: \.primeModal ) )

12:10

Everything should build, but we need to make sure that when the nth prime button is tapped, we wrap our CounterAction with a CounterViewAction . self.store.send(.counter(.nthPrimeButtonTapped))

12:20

Things are building, but we still need to update the views, which have up till now been relying on local state. public struct CounterView: View { @ObservedObject var store: Store<CounterViewState, CounterViewAction> @State var isPrimeModalShown = false @State var alertNthPrime: PrimeAlert? @State var isNthPrimeButtonDisabled = false

12:28

Previously alertNthPrime and isNthPrimeButtonDisabled was local state managed by SwiftUI using the @State property wrapper. But now we’ve moved this state into the store, so we should be able to comment them out. // @State var alertNthPrime: PrimeAlert? // @State var isNthPrimeButtonDisabled = false

12:41

And now we can update the views to use the store’s state instead. Like whether or not the nth prime button is disabled. .disabled(self.store.value.isNthPrimeButtonDisabled)

12:53

And whether or not to show an alert. .alert(item: self.store.value.alertNthPrime) { alert in

13:09

Unfortunately, this interface expects a SwiftUI binding, not a value. So what are we to do here? We could perhaps see if it’s possible to construct a binding ourselves, which has an initializer that takes a get function and a set function. Binding(get: <#() -> _#>, set: <#(_) -> Void#>)

13:28

The get function can return a store’s value, while its setter can be a no-op closure. Binding(get: { self.store.value.alertNthPrime }, set: { _ in })

13:53

And now, everything builds just fine!

13:55

This doesn’t look great, though: .alert( item: Binding( get: { self.store.value.alertNthPrime }, set: { _ in } ) ) { alert in It’s very noisy, and introducing so much noise for something as simple and as common as an alert binding isn’t great.

14:07

Luckily, SwiftUI provides a Binding helper that does this work for us! It’s called a “constant” binding and is available as a static method. .alert( item: Binding.constant(self.store.value.alertNthPrime) ) { alert in

14:24

We can use type inference to shorten things up. .alert( item: .constant(self.store.value.alertNthPrime) ) { alert in

14:28

So this provides a nice way of building SwiftUI bindings for our unidirectional architecture.

14:44

Everything is now building, but to take things for a spin we must finally make the CounterViewState initializer public, since we’ve upgraded it from a tuple to a struct. public struct CounterViewState { var alertNthPrime: PrimeAlert? var count: Int var favoritePrimes: [Int] var isNthPrimeButtonDisabled: Bool public init( alertNthPrime: PrimeAlert?, count: Int, favoritePrimes: [Int], isNthPrimeButtonDisabled: Bool ) { self.alertNthPrime = alertNthPrime self.count = count self.favoritePrimes = favoritePrimes self.isNthPrimeButtonDisabled = isNthPrimeButtonDisabled }

15:24

We can now use this initializer in the playground and take things for a spin! PlaygroundPage.current.liveView = UIHostingController( rootView: CounterView( Store: Store<CounterViewState, CounterViewAction>( initialValue: CounterViewState( alertNthPrime: nil, count: 0, favoritePrimes: [], isNthPrimeButtonDisabled: false ), reducer: counterViewReducer ) ) )

15:47

The playground builds, runs, and we can still ask Wolfram Alpha for a specific prime. The async signature

15:59

It took some time to get here, because our asynchronous side effect was entangled in some local state, and moving everything into the reducer caused a bit of a ripple effect.

16:17

Unfortunately, the way we implemented our asynchronous effect is not a good solution. For one thing, this effect is now blocking, which means no other effects can run till it’s completed. While we haven’t finished hooking everything up, let’s at least explore why this is problematic.

16:39

If we go to the counter screen, and ask for the 5th prime, we will notice that the interface freezes while we wait for the network request to finish. This is of course very bad.

17:15

Now one thing we could do of course is completely change how our store runs effects in order to support asynchrony: DispatchQueue.global().async { effects.forEach { effect in if let action = effect() { DispatchQueue.main.async { self.send(action) } } } }

17:50

And we can re-run the playground to see that things are working a bit nicer.

18:09

But this is problematic for a couple reasons.

18:11

We are hard-coding a dispatch queue to run our async work on, and worst of all this is an inflexible choice being made for all users of the store. Ideally we would let each effect decide just how asynchronous they want to be.

18:36

Our asynchronous effect has to manage its asynchrony internally using a GCD semaphore. This is just a super clunky way of handling the problem. It’d be nice if we could re-model effects to handle asynchrony in a more natural way that didn’t require creating semaphores that need to be told to wait and signaled later on.

18:50

Luckily for us, we have discussed a type on Point-Free many times that represents the essence of asynchronous work. We have previously called this the Parallel type, and it looked like the following: struct Parallel<A> { let run: (@escaping (A) -> Void) -> Void }

19:08

This signature, although strange at first glance, is a shape that we encounter frequently in iOS development. It is the precise shape for many asynchronous APIs we deal with on regular basis, such as dispatch async, UIView animation, and URL session data tasks: DispatchQueue.main.async(execute: () -> Void) -> Void UIView.animate( withDuration: TimeInterval, animations: () -> Void ) -> Void URLSession.shared.dataTask( with: URL, completionHandler: (Data?, URLResponse?, Error?) -> Void ) -> Void

20:06

This signature is what allows us to hand over control to the function we are invoking so that they can give us values back when they are ready, rather than demanding a value immediately.

20:17

It seems promising that the Parallel type may be able to help us here!

20:34

We first explored this type when trying to understand the properties and universality of the map , zip and flatMap operations.

20:45

But then, a few months later we saw that it could also be used to fix a critical problem with our snapshot testing library. We first built the library so that when snapshotting a value you needed to perform the work synchronously and produce a value immediately. But, we found out that some objects need to do some work asynchronously before they can be snapshot, such as interfaces that use delegates to inform users when they are done. Even worse, some of those objects require the main thread to do their work, such as WKWebView , which means it became impossible for us to snapshot them in a synchronous fashion from a test case that was running on the main thread without deadlocking. So, we decided we needed to introduce some asynchrony into the library, and the parallel type was the perfect tool.

21:42

We basically have to repeat that story for our effects right now. We need to throw away this synchronous effect signature that requires us to immediately return an optional action, and instead use an asynchronous signature that allows us to supply the action when we are ready. The async effect

21:55

So, let’s update our effect signature once again and see what all needs to be fixed: public typealias Effect<Action> = (@escaping (Action) -> Void) -> Void

22:15

First thing that breaks is that our send method on the store is no longer correct. We don’t want the store to handle the dispatch async logic. Instead we need to run each of our effects by providing a callback, and then when that callback is invoked we will forward the resulting action on to our send method: public func send(_ action: Action) { let effects = self.reducer(&self.value, action) effects.forEach { effect in effect { action in self.send(action) } } }

23:00

Where we pass effect the callback closure is where we are given the opportunity to perform some async work, because the effect itself gets to decide when to feed an action back to send .

23:17

We can even shorten things by passing self.send as the callback value. effects.forEach { effect in effect(self.send) }

23:23

Next, we’ve got our pullback function. It needs to transform its local effects into global effects by running the effects, and then using the callback to embed the local effect into the global effect: public func pullback< LocalValue, GlobalValue, LocalAction, GlobalAction >( _ reducer: @escaping (inout LocalValue, LocalAction) -> [ Effect<LocalAction> ], value: WritableKeyPath<GlobalValue, LocalValue>, action: WritableKeyPath<GlobalAction, LocalAction?> ) -> (inout GlobalValue, GlobalAction) -> [Effect<GlobalAction>] { return { globalValue, globalAction in guard let localAction = globalAction[keyPath: action] else { return [] } let localEffects = reducer( &globalValue[keyPath: value], localAction ) return localEffects.map { localEffect in { callback in // guard let localAction = localEffect() else { return nil } localEffect { localAction in var globalAction = globalAction globalAction[keyPath: action] = localAction callback(globalAction) } } } } }

24:34

The only other thing not compiling in our composable architecture module is the logging higher order reducer, which is easy enough to fix. We can merely ignore the callback argument and get rid of the return statement. public func logging<Value, Action>( _ reducer: @escaping (inout Value, Action) -> [Effect<Action>] ) -> (inout Value, Action) -> [Effect<Action>] { return { value, action in let effects = reducer(&value, action) let newValue = value return [ { _ in print("Action: \(action)") print("Value:") dump(newValue) print("---") } ] + effects } }

24:53

Now let’s attack the problems in the other modules. First we got the counter module. All the places we were returning empty arrays [] are still okay, but the spots we were constructing some effect need to be updated, such as when the nth prime button is tapped: case .nthPrimeButtonTapped: state.isNthPrimeButtonDisabled = true let count = state.count return [{ callback in nthPrime(count) { prime in callback(.nthPrimeResponse(prime)) } }] Refactor-related bugs

25:44

The counter module is now in building order and this effect is now much simpler.

25:55

But when we take the counter playground for another spin, we’ll see that things aren’t quite working as we’d expect. For one thing, when we tap the nth prime button, it seems to be taking quite some time to get a response.

26:20

We can add some logging to the reducer to debug things with the logging higher-order reducer we defined in an earlier episode . reducer: logging(counterViewReducer)

26:38

And when we tap the button, we’ll see that we get an nthPrimeResponse in long before the alert shows up onscreen.

26:53

By moving this async effect into the reducer and having the action dispatched to the store, we’ve introduced a problem that SwiftUI was previously taking care of for us.

27:07

The problem is that we’re mutating the store’s value on a background thread. URLSession data tasks execute their completion blocks on background threads by default, which means we’re sending actions back to the store on a background thread, which means the store’s value is being mutated on a background thread, which means the value is being published to SwiftUI on a background thread.

27:39

Before, the state lived in the view, and we were updating it through the view’s @State -wrapped property. This seemed to ensure that mutations were always happening on the main thread. But now we’ve moved this state into a store and are publishing those updates directly via the ObservableObject and @Published machinery.

27:54

The fix is easy enough. We can update the effect to dispatch its result on the main queue: nthPrime(n) { prime in DispatchQueue.main.async { callback(.nthPrimeResponse(prime)) } }

28:08

And when we build things and run, we’ll see that the alert now pops up immediately as the response comes back, and we no longer have those warnings in the console.

28:26

However, if we tap the button a second time…the alert never shows! We can check the console to confirm that a response is definitely coming in, so what’s the issue? Even weirder, when we ask for other favorite primes we can sometimes get a crash. Thinking unidirectionally

28:46

We appear to be missing something else. While we fixed the threading issue, we’re seeing something new.

29:01

This may have to do with how we’ve interfaced our architecture with SwiftUI’s alert binding API. Normally, you handle control over to SwiftUI for certain kinds of presentations using bindings, so that SwiftUI can nil out some state for you. But we used a “constant” binding, which might prevent our state from properly nil -ing things out on alert dismissal.

29:27

The issue is with how this state is being mutated: case let .nthPrimeResponse(nthPrime): state.alertNthPrime = nthPrime

29:33

While the reducer sets the nthPrime with the result of a response. It never nil s this state out.

29:39

We could try to nil things out when the button is tapped again. case .nthPrimeButtonTapped: state.alertNthPrime = nil

29:54

And this allows us to request a prime multiple times in a row.

30:11

But waiting for another button tap isn’t the right place to nil out the alert state. It would make more sense for the alert state to go away when the alert is dismissed. But we’re using a constant binding, so SwiftUI cannot write to this state automatically when the alert is dismissed by nil -ing it out for us. .alert( item: .constant(self.store.value.nthPrime) ) { alert in Alert( title: Text( """ The \(ordinal(self.store.value.count)) prime is \ \(alert.prime) """ ), dismissButton: .default(Text("OK")) ) }

30:42

Alert buttons can optionally take actions. Alert.Button.default(<#label: Text#>, action: <#(() -> Void)?#>)

30:47

This is important for alert choices that should perform some task. For example, an alert with a confirmation button should perform that task only if the confirmation button is tapped. In our case we can use this block to feed a dismissal action back into the store that nil s out the alert state. Alert( title: Text( """ The \(ordinal(self.store.value.count)) prime is \ \(alert.prime) """ ), dismissButton: .default(Text("OK")) { self.store.send(.counter(.alertDismissButtonTapped)) } )

31:08

We need to introduce this action to our CounterAction type. public enum CounterAction { case alertDismissButtonTapped

31:19

And we need to handle it in our reducer. case .alertDismissButtonTapped: state.alertNthPrime = nil return []

31:36

When we build and run the playground, we can now ask for a prime, dismiss the alert and see this dismissal reflected in the logs.

31:54

We’ve finally fully extracted the most complex, asynchronous side effect in our application to work in our architecture to work exactly as it did before. There were a few bumps along the way, but we were able to address every single one of them.

32:09

This effect was definitely more complicated than the others, for two reasons:

32:15

First, this effect was bundled up with the idea of showing and dismissing alerts, which is something we hadn’t previously considered in our architecture. Solving it required us to consider what it means to extract out local state associated with the alert presentation, how to manage bindings, dismissal, and so on. Most of the bugs were around that, so in the future we’ll explore better means of interfacing with SwiftUI APIs.

32:40

Second, the effect was asynchronous! It’s just inherently more complex than a synchronous effect. We needed to take into account a threading issue, though it’s not a fault of the architecture and is an issue that anyone extracting logic from a view to an observable object would encounter.

33:21

We now have the type of our effect: that Parallel -like shape, where you get to hand off a function to someone else, where they get to do their work and invoke that function when it’s ready. And we have the shape of our reducer, which can return any number of effects in an array, where the results can be fed back into the store. And it was cool to see that we were able to, once again, do an async functional refactoring and have everything just work in the end.

34:00

We were also able to embrace the idea of “unidirectional data flow.” Even with the complications that effects and asynchronicity introduce, we’re still able to reason about how data flows through the application, because effects can only mutate our app’s state through actions send back through the store. The store is the single entryway for mutations. This is the basic version of the story for effects in our architecture. What’s the point?

34:35

This is all very cool stuff, but at the end of each series of episodes on Point-Free we like to be a little reflective on what has been accomplished and ask “what’s the point?” And in this case, although it was cool we were able to refactor some code out of the views and into the reducers, it’s really feeling like we are just shuffling code around. Rather than that side-effecty code living inside a closure in the view, it now lives inside a closure in the reducer. Is that really accomplishing anything? What was the point of this refactor?

34:58

Although it seems like just a code reshuffling, it’s actually just the first step of many in turning our reducers into robust units of business logic that will power our entire applications. We’ve already seen that reducers are a really wonderful, composable unit to run our state management and mutations off of, so it stands to reason that letting them also be responsible for producing side effects might also have a lot of benefits. So let’s take a look at that…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 0078-effectful-state-management-async-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 .