Effectful State Management: The Point

Episode #79 • Nov 4, 2019 • Subscriber-Only

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.

The Point
Introduction
00:05
What’s the point?
02:47
Composable, transformable effects
04:13
Reusable effects: network requests
11:16
Reusable effects: threading
17:02
Getting everything building again
20:59
Conclusion
26:26

Unlock This Episode

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

Introduction

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.

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

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.

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.

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.

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.

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. Right now the isPrime function is defined as a little helper in IsPrimeModal.swift. It’s a very straightforward way of checking primes, but it can be slow for very large primes. For example, using the Counter playground, start the application’s state with the following very large prime number:

    21,111,111,111,113
    

    That’s a two followed by 12 ones and a three! If you ask if that number is prime, the UI will hang for about 5 seconds before showing the modal. Clearly not a great user experience.

    To fix this, upgrade the isPrime helper to an isPrime: Effect<Bool> so that it can be run on a background queue. Make sure to also use receive(on: .main) so that the result of the effect is delivered back on the main queue.

  2. In the previous exercise you probably used a DispatchQueue directly in the definition of the isPrime effect. Rather than hard coding a queue in the effect, implement the following function that allows you to determine the queue an effect will be run from:

    extension Effect {
      func run(on queue: DispatchQueue) -> Effect {
        fatalError("Unimplemented")
      }
    }
    
  3. A “higher-order effect” is a function that takes an effect as input and returns an effect as output. This allows you to enrich an existing effect with additional behavior. We’ve already seen a few examples of this, such as map and receive(on:). The next few exercises will walk you through writing a cancellation higher-order effect.

    Start by implementing an effect transformation of the form:

    extension Effect {
      func cancellable(id: String) -> Effect {
        fatalError("Unimplemented")
      }
    }
    

    This enriches an existing Effect with the behavior that allows it to be canceled at a later time. To achieve this, record whether or not a particular effect has been canceled by maintaining a private [String: Bool] dictionary at the file scope, and use the boolean to determine if future effect values should be delievered.

  4. Continuing the previous exercise, implement an effect that can cancel an in-flight effect with a particular id:

    extension Effect {
      static func cancel(id: String) -> Effect {
        fatalError("Unimplemented")
      }
    }
    
  5. Continuing the previous exercise, in the counterReducer, cancel an in-flight nthPrime effect whenever either the increment or decrement buttons are tapped.

  6. Continuing the previous exercise, improve the implementations of cancellable and cancel by:

    • Allow for any Hashable id, not just a String.
    • Use a DispatchWorkItem to represent the cancellable unit of work instead of a Bool.
    • Use a os_unfair_lock to properly protect access to the private dictionary that holds the DispatchWorkItem’s.
  7. Using the previous exercise on cancellation as inspiration, create a similar higher-order effect for debouncing an existing effect:

    extension Effect {
      public func debounce<Id: Hashable>(
        for duration: TimeInterval,
        id: Id
      ) -> Effect {
        fatalError("Unimplemented")
      }
    }
    

    This should cancel any existing in-flight effect with the same id while delay the current effect by the duration passed.

  8. Use the debounce higher-order effect to implement automatic saving of favorite primes by debouncing any AppAction by 10 seconds and then performing the save effect.

  9. Consider an effect of the form Effect<Never>. What can be said about how such an effect behaves without knowing anything about how it works internally?

  10. Implement the following function for transforming an Effect<Never> into an Effect<B>:

    extension Effect where A == Never {
      func fireAndForget<B>() -> Effect<B> {
        fatalError("Unimplemented")
      }
    }
    
  11. Consider an analytics client that can track events by using an Effect:

    struct AnalyticsClient {
      let track: (String) -> Effect<???>
    }
    

    What type of generic should be used for the Effect?

  12. Construct a live implementation of the above analytics client:

    extension AnalyticsClient {
      static let live: AnalyticsClient = ???
    }
    

    For now you can just perform print statements, but in a real production application you could make an API request to your analytics provider.

    Use the above live analytics client to instrument the reducers in the PrimeTime application. You may find the fireAndForget function to be helpful for using the analytics effects in our reducers.

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.

Why Functional Programming Matters

John Hughes • Saturday 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.

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