A video series exploring functional programming and Swift.
#78 • Monday Oct 28, 2019 • Subscriber-only

Effectful State Management: Asynchronous Effects

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.

This episode builds on concepts introduced previously:

#78 • Monday Oct 28, 2019 • Subscriber-only

Effectful State Management: Asynchronous Effects

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.

This episode builds on concepts introduced previously:


Subscribe to Point‑Free

This episode is for subscribers only. To access it, and all past and future episodes, become a subscriber today!

See subscription optionsorLog in

Sign up for our weekly newsletter to be notified of new episodes, and unlock access to any subscriber-only episode of your choosing!

Sign up for free episode

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.

Subscribe to Point-Free

👋 Hey there! Does this episode sound interesting? Well, then you may want to subscribe so that you get access to this episodes and more!


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”.

Chapters
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