Effectful State Management: Asynchronous Effects

Episode #78 • Oct 28, 2019 • Subscriber-Only

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.

Asynchronous Effects
Introduction
00:05
Extracting our asynchronous effect
02:17
Local state to global state
06:25
The async signature
15:59
The async effect
21:55
Refactor-related bugs
25:44
Thinking unidirectionally
28:46
What’s the point?
34:35

Unlock This Episode

Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.

Introduction

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

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.

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.

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.

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.

This episode is for subscribers only.

Subscribe to Point-Free

Access this episode, plus all past and future episodes when you become a subscriber.

See plans and pricing

Already a subscriber? Log in

Exercises

  1. Our Effect is currently just a type alias:

    typealias Effect<Action> = (@escaping (Action) -> Void) -> Void
    

    Upgrade it to be a struct wrapper around a function, like the parallel type:

    struct Parallel<A> {
      let run: (@escaping (A) -> Void) -> Void
    }
    

    Make the necessary changes to get the application building again.

  2. Define map as a method on Effect.

    struct Effect<A> {
      …
      func map<B>(_ f: @escaping (A) -> B) -> Effect<B> {
        fatalError("TODO")
      }
    
  3. Use Effect’s map method to decouple the work of a side effect from the work that wraps its result in a reducer action. For example: rather than explicitly wrap the nth prime in an nthPrimeResponse in the main effect, chain that work into a map on an Effect<Int?>.

  4. Extend Effect with a receive(on queue: DispatchQueue) method to decouple the async-on-main work we did when handling the nth prime response. Update the nth prime effect to use this method.

  5. Define zip on Effect.

    func zip<A, B>(_ a: Effect<A>, _ b: Effect<B>) -> Effect<(A, B)> {
      fatalError("TODO")
    }
    

    What might this function be useful for? If you are new to the zip function, we devoted an entire series to the concept, starting here.

  6. Define flatMap on Effect.

    struct Effect<A> {
      …
      func flatMap<B>(_ f: @escaping (A) -> Effect<B>) -> Effect<B> {
        fatalError("TODO")
      }
    

    What might this function be useful for? If you are new to the flatMap function, we devoted an entire series to the concept, starting here.

  7. When we incorporated alert presentation into our architecture, we needed to explicitly introduce a dismissal event to nil out alert state. While we did so with an alert button action, an alternative would have been to let SwiftUI feed a dismiss action to the store via the binding.

    Rewrite the alert dismissal to use a non-constant Binding with a setter that sends a dismiss action to the store.

  8. Write a helper method on Store that simplifies the presentation of optional sub-state by returning a binding of an optional sub-store.

    func presentation<PresentedValue>(
      _ value: KeyPath<Value, PresentedValue?>,
      dismissAction: Action
    ) -> Binding<Store<PresentedValue, Action>?>
    

    Use this method to present the nth prime alert.

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.

Redux: Data Flow

The Redux documentation describes and motivates its “strict unidirectional 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.

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.

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.

SwiftUIFlux

Thomas Ricouard

An early example of Redux in SwiftUI. Like ReSwift, it uses “middleware” to handle side effects.

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.

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.

Composable Reducers

Brandon Williams • Tuesday 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”.