EP 79 · Effectful State Management · Nov 4, 2019 ·Members

Video #79: Effectful State Management: The Point

smart_display

Loading stream…

Video #79: Effectful State Management: The Point

Episode: Video #79 Date: Nov 4, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep79-effectful-state-management-the-point

Episode thumbnail

Description

We’ve got the basic story of side effects in our architecture, but the story is far from over. Turns out that even side effects themselves are composable. Base effect functionality can be extracted and shared, and complex effects can be broken down into simpler pieces.

Video

Cloudflare Stream video ID: 9bd161dee2de3182eac2f0c4a096254f Local file: video_79_effectful-state-management-the-point.mp4 *(download with --video 79)*

References

Transcript

0:05

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.

0:15

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

0:27

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.

0:52

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.

1:34

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.

2:11

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?

2:47

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?

3:09

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.

3:59

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. Composable, transformable effects

4:13

However, it’s kind of hard to see those benefits right now because of how the effect type is expressed. Right now it’s a simple type alias of function signature. When we first discussed the shape of this signature, back when it was called Parallel , we wrapped it in a struct and defined a map operation on it so that we could transform it in a lightweight way. Let’s do that and see what happens: public struct Effect<A> { public let run: (@escaping (A) -> Void) -> Void public init(run: @escaping (@escaping (A) -> Void) -> Void) { self.run = run } public func map<B>(_ f: @escaping (A) -> B) -> Effect<B> { return Effect<B> { callback in self.run { a in callback(f(a)) } } }

5:14

Now this caused a number of compiler errors that we have to fix, but before fixing them let’s remember how powerful it can be to have a single, transformable unit that expresses some part of our application. We’ve seen this time and time again on Point-Free. Whether it be styling functions, or random number generators, or parsers, or snapshot testing, or state management, as long as you have a simple, well-understood unit that is capable of being transformed, amazing things will come out of it.

5:42

And we are now seeing that perhaps side effects is yet another concept that fits into that narrative. We have a base unit that is quite simple, it’s just a unit of work that can be performed asynchronously via some callback function. And it supports a map operation so that we can easily transform whatever value is produced by the effect into another value. We could also ask if effects support a zip operation, or a flat map operation, or what “higher-order effects” look like, but even before all of that we can gain a lot of benefit from just this map operation.

6:34

In order to see this, let’s get ourselves in building order again. The majority of what we need to do is add some .run ’s in a few spots, and explicitly specify the Effect name when returning closures: // ComposableArchitecture.swift // effect(self.send) effect.run(self.send) // return { callback in // localEffect { localAction in return Effect { callback in localEffect.run { localAction in // { _ in Effect { _ in

7:48

Now that we are in building order we can start to see how the effect type will allow us to make sense of some our side effecting work, and even simplify. If we go over to our Wolfram Alpha swift file we will see the guts of our side effect: public func wolframAlpha( query: String, callback: @escaping (WolframAlphaResult?) -> Void ) -> Void { var components = URLComponents( string: "https://api.wolframalpha.com/v2/query" )! components.queryItems = [ URLQueryItem(name: "input", value: query), URLQueryItem(name: "format", value: "plaintext"), URLQueryItem(name: "output", value: "JSON"), URLQueryItem(name: "appid", value: wolframAlphaApiKey), ] URLSession.shared .dataTask( with: components.url(relativeTo: nil)! ) { data, response, error in callback( data.flatMap { try? JSONDecoder() .decode(WolframAlphaResult.self, from: $0) } ) } .resume() } func nthPrime(_ n: Int, callback: @escaping (Int?) -> Void) -> Void { wolframAlpha(query: "prime \(n)") { result in callback( result .flatMap { $0.queryresult .pods .first(where: { $0.primary == .some(true) })? .subpods .first? .plaintext } .flatMap(Int.init) ) } }

8:08

And let’s also paste in how we use this code in the reducer so that we can have everything together: return [ Effect { callback in nthPrime(n) { prime in DispatchQueue.main.async { callback(.nthPrimeResponse(prime)) } } } ]

8:19

The first strange thing I see here is that the library code for dealing with Wolfram Alpha is still living in callback world rather than using the Effect type. The next thing I notice is that the library Wolfram code is mixing together some universal things that have nothing to do with interacting with the Wolfram API and things that are domain specific to Wolfram, such as the code that runs the request from URLSession and the code that does JSON decoding. Let’s address some of these strange things.

9:16

First, let’s make the wolframAlpha query function return an effect instead of leveraging a callback API: public func wolframAlpha( query: String ) -> Effect<WolframAlphaResult?> { return Effect { callback in

10:19

This breaks the nthPrime function, but that too should be updated to work with effects, and it’s now quite simple because we can just map on the Wolfram query effect: func nthPrime(_ n: Int) -> Effect<Int?> { return wolframAlpha(query: "prime \(n)").map { result in Reusable effects: network requests

11:16

So, it’s nice that our domain specific Wolfram effects are expressed in the Effect type, but now this means we can look to leverage the map operation to split up these units a bit to make them more understandable. For example, we could introduce an effect that is only responsible for executing a URLSession request: func dataTask( with request: URL ) -> Effect<(Data?, URLResponse?, Error?)> { return Effect { callback in URLSession.shared.dataTask( with: request ) { data, response, error in callback((data, response, error)) } .resume() } } This code is library code that could be completely separated from our application and any of its modules.

12:26

And then we can leverage this base unit of work to implement our Wolfram API effect: public func wolframAlpha( query: String ) -> Effect<WolframAlphaResult?> { var components = URLComponents( string: "https://api.wolframalpha.com/v2/query" )! components.queryItems = [ URLQueryItem(name: "input", value: query), URLQueryItem(name: "format", value: "plaintext"), URLQueryItem(name: "output", value: "JSON"), URLQueryItem(name: "appid", value: wolframAlphaApiKey), ] return dataTask(with: components.url(relativeTo: nil)!) .map { data, _, _ in data.flatMap { try? JSONDecoder() .decode(WolframAlphaResult.self, from: $0) } } }

13:53

But there’s another little unit of reusable code sitting in here: the JSON decoding. What if we could simply chain the concept of JSON decoding onto any effect? Sounds like we should extend the Effect type with a method to do this logic: extension Effect where A == (Data?, URLResponse?, Error?) { func decode<B: Decodable>(as type: B.Type) -> Effect<B?> { return self.map { data, _, _ in data.flatMap { try? JSONDecoder().decode(B.self, from: $0) } } } }

15:25

And now we can further simplify our Wolfram API effect so that it’s mostly only concerned with Wolfram-specific logic, such as constructing the request for the Wolfram API, and not the concerns of making networks requests and doing JSON decoding: return Effect.dataTask(with: components.url(relativeTo: nil)!) .decode(as: WolframAlphaResult.self)

15:56

Note that we could substantially beef this up by using a proper Result type instead of just optionals.

16:07

Now to actually use this we need to update how we use this effect in our reducer. Currently it looks like this: Effect { callback in nthPrime(n) { prime in DispatchQueue.main.async { callback(.nthPrimeResponse(prime)) } } }

16:27

Now that the nthPrime function returns a prime, we don’t need to explicitly create an effect and call the function under the hood. We can just invoke it directly: nthPrime(count) // Effect<Int?>

16:40

Only strange thing here is that this effect produces an optional integer, but to be returned from the reducer it must be an effect of a CounterAction . How can we transform this optional integer into an action? Well, with map of course! And in fact, we can even use the nthPrimeResponse case of the CounterAction as the function that we want to map with: nthPrime(count).map { CounterAction.nthPrimeResponse($0) } Reusable effects: threading

17:02

That will get us compiling, but we have somehow lost the ability to make sure we dispatch back to main when sending our action. We can’t add this functionality to our effect using map alone. The map operation has no way of changing how actions are delivered to the callback, it can only do simple transformations to the values before they are sent to the callback. However, we can define another operation on Effect , much like we did for JSON decoding, that adds this functionality: extension Effect { func receive(on queue: DispatchQueue) -> Effect { return Effect { callback in self.run { a in queue.async { callback(a) } } } } }

19:16

And now our transformation of the effect becomes: nthPrime(n) .map(CounterAction.nthPrimeResponse) .receive(on: .main) let count = state.count return [ nthPrime(count) .map(CounterAction.nthPrimeResponse) .receive(on: .main) And now this effect is starting to look really nice and declarative. The nthPrime function gets to focus just on the work it needs to do to compute the nth prime (and even that was broken down into small units), then we map on that effect to massage it into the counter action that we want to send back into the store, and finally we make sure that action is sent on the main thread, which is necessary since the nthPrime does its work on a background thread.

20:08

And even better, since we aren’t operating in the closure of a manually constructed effect, we get to drop the let dance that we had to do earlier for our mutable value: return [ nthPrime(state.count) .map(CounterAction.nthPrimeResponse) .receive(on: .main)

20:41

After all of these updates, we’ve unfortunately broken things, so let’s do a little work to get things back into working order. Getting everything building again

20:59

If we check the favorite primes module, it isn’t it building order because its reducer is still speaking single, synchronous effects. We need to update it to return an async array of closures. private func saveEffect( favoritePrimes: [Int] ) -> Effect<FavoritePrimesAction> { return Effect { _ in … } }

21:29

The load effect is in a similar state, but we need to use the Effect initializer, introduce the callback parameter, and pass things along. private let loadEffect = Effect<FavoritePrimesAction>.sync { 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 } // self.store.send(.loadedFavoritePrimes(favoritePrimes)) callback(.loadedFavoritePrimes(favoritePrimes)) }

21:54

The favorite primes module is now building, so let’s try to get the main app building alongside it. There are quite a few errors, but let’s try to fix them one by one.

22:11

The activity feed broke because it doesn’t speak effects and because it has an exhaustive switch. Let’s update the signature. func activityFeed( _ reducer: @escaping (inout AppState, AppAction) -> [Effect<AppAction>] ) -> (inout AppState, AppAction) -> [Effect<AppAction>] {

22:25

And make it exhaustive. case .counterView(.counter), .favoritePrimes(.loadedFavoritePrimes), .favoritePrimes(.loadButtonTapped), .favoritePrimes(.saveButtonTapped):

22:49

Finally, instead of merely running the reducer, we must also return its effects. return reducer(&state, action)

23:02

Things are closer to building, but state has changed. When we project AppState into CounterViewState , we must plumb a few new values along. We can updated AppState accordingly. struct AppState { … var alertNthPrime: PrimeAlert? = nil var isNthPrimeButtonDisabled: Bool = false

23:50

Introducing this top-level state might seem like it’s getting out of hand, but we will address normalizing app state in a future episode.

24:02

Now that this state is available on the app level, we can pass it along to our counter view. extension AppState { var counterView: CounterViewState { get { CounterViewState( alertNthPrime: self.alertNthPrime, count: self.count, favoritePrimes: self.favoritePrimes, isNthPrimeButtonDisabled: self.isNthPrimeButtonDisabled ) } set { self.alertNthPrime = newValue.alertNthPrime self.count = newValue.count self.favoritePrimes = newValue.favoritePrimes self.isNthPrimeButtonDisabled = newValue .isNthPrimeButtonDisabled } } } And we must also make these properties public.

25:20

Everything now builds, and if we take things for a spin, it all still behaves as before. But it would be nice if the reusable effect logo we wrote in the counter module would be accessible everywhere. Luckily we can move it to ComposableArchitecture without much work.

26:13

All we need to do is copy, paste, and make public. Everything builds just fine otherwise. Conclusion

26:26

So now we have the ability to share very reusable effects in the base composable architecture module. But even better, you may want to share more domain specific effects!

26:40

And this is the point of everything we have done with effects. We have stumbled upon another composable concept in our architecture: side effects! And this might seem pretty surprising at first. We are so used to just sprinkling side effects throughout our code bases to get things working, and over time it gets harder and harder to fully understand how all of these effects work together to get the job done. We don’t think of them as having a well-defined structure that can be manipulated at a high level.

26:53

There’s still a lot to say about effects in this architecture because we have only just begun to scratch the surface of why composability in an effect is important. But before we go any deeper into that, let’s check back into the goals of our architecture that we started about 14 episodes ago.

26:58

We have laid out 5 important problems that any architecture should aim to solve:

27:08

The types of the architecture should be expressed as simple value types. We’ve seen time and time again that value types can clear away a lot of complexity, so we should aim to embrace them as much as possible.

27:20

The base units of mutation and observation of state should be expressed in a way that is composable. There should be operations that allow you to derive all new forms of mutation and observation from existing ones.

27:37

The architecture should be modular, that is you should literally be able to put many units of the architecture into their own module so that they are fully separated from everything else while still having the ability to be glued together.

27:56

The architecture should have an explicit story for side effects. It should describe precisely how side effects are performed, and how the result of their work is fed back into the architecture.

28:06

And finally, the architecture should describe how one tests the various components, and ideally a minimal amount of set up is needed to write these tests.

28:15

We have now provided very precise answers to 4 of these 5 problems. Next time we will begin working on testing, and show that it is possible to get amazing test coverage in this architecture. It may even be pretty surprising considering the work we did in this episode, because this Effect type may not seem super testable right now. However, it is possible, and it’s pretty cool.

28:33

Until 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 Why Functional Programming Matters John Hughes • Apr 1, 1989 A classic paper exploring what makes functional programming special. It focuses on two positive aspects that set it apart from the rest: laziness and modularity. https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf 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 0079-effectful-state-management-wtp 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 .