Composable State Management: Reducers

Episode #68 • Aug 5, 2019 • Subscriber-Only

Now that we understand some of the fundamental problems that we will encounter when building a complex application, let’s start solving some of them! We will begin by demonstrating a technique for describing the state and actions in your application, as well as a consistent way to apply mutations to your application’s state.

Reducers
Introduction
00:06
Recap: our app so far
03:19
A better way to model global state
09:02
Functional state management
14:36
Ergonomics: capturing reducer in store
24:02
Ergonomics: in-out reducers
27:52
Moving more mutations into the store
32:56
Till next time
40:05

Unlock This Episode

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

Introduction

The past few weeks we have explored the problem space of application architecture, and tried to uncover the root of what makes it so complicated. We ended up building a moderately complex application in the process, and although it was a bit of a toy example, it accented all of the pain points we encounter when building applications. In particular, we saw:

  • We want to be able to have complex app state that can be shared across many screens so that a mutation to one part of the state is reflected in the other screens instantaneously.
  • We want to be able to mutate the state in a consistent manner so that it’s obvious to newcomers to our code base how the data flows through the application.
  • We want to be able to build large, complex applications out of simple, composable units. Ideally we’d be able to build a component in full isolation, possibly even in its own module, and then later plug that component into a much bigger application.
  • We would like a well defined mechanism for executing side effects and feeding their results back into the application.
  • And finally we would like our architecture to be testable. Ideally we should be able to write tests with very little setup that allow us to describe a series of actions a user does in the app and then assert on the state of the app after those actions have been performed.

These are very important problems to solve because they allow us to scale our code base to handle many features and many developers working on the same app. Unfortunately, SwiftUI doesn’t solve these problems for us completely. It gives us many of the tools to solve it for ourselves, but it is up to us to take things the extra mile.

And so today we begin doing just that. We will introduce an application architecture that solves these problems. It’s opinionated in much the same way that SwiftUI is. It tells us exactly how we are supposed to model application state, tells us how mutations are applied to that state, tells us how to execute side effects and more. If we follow these prescriptions some really amazing benefits will start to pop up. And of course, the most important part, this architecture is entirely inspired by functional programming! We will draw inspiration from simple functions and function composition in order to understand how we can solve all of these problems.

We of course don’t claim that this architecture is a panacea and will solve all of your problems, and there will definitely be times where it seems that the problem you are working on simply does not fit this framework. However, we still feel that it’s worth exploring these ideas, and it can also be surprising how many problems can be solved with this architecture if you look at the problem from the right angle.

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. In this episode we remarked that there is an equivalence between functions of the form (A) -> A and functions (inout A) -> Void, which is something we covered in our episode on side effects. Prove this to yourself by implementing the following two functions which demonstrate how to transform from one type of function to the other:

    func toInout<A>(_ f: @escaping (A) -> A) -> (inout A) -> Void {
      fatalError("Unimplemented")
    }
    
    func fromInout<A>(_ f: @escaping (inout A) -> Void) -> (A) -> A {
      fatalError("Unimplemented")
    }
    
    Solution
    func toInout<A>(_ f: @escaping (A) -> A) -> (inout A) -> Void {
      return { inoutA in
        let updatedA = f(inoutA)
        inoutA = updatedA
      }
    }
    
    func fromInout<A>(_ f: @escaping (inout A) -> Void) -> (A) -> A {
      return { a in
        var mutableA = a
        f(&mutableA)
        return mutableA
      }
    }
    
  2. Our appReducer is starting to get pretty big. Right now we are switching over an enum that has 5 cases, but for a much larger application you may have dozen or even hundreds of cases to consider. This clearly is not going to scale well.

    It’s possible to break up a reducer into smaller reducers by implementing the following function:

    func combine<Value, Action>(
      _ first: @escaping (inout Value, Action) -> Void,
      _ second: @escaping (inout Value, Action) -> Void
    ) -> (inout Value, Action) -> Void {
      fatalError("Unimplemented")
    }
    

    Implement this function.

    Solution
    func combine<Value, Action>(
      _ first: @escaping (inout Value, Action) -> Void,
      _ second: @escaping (inout Value, Action) -> Void
    ) -> (inout Value, Action) -> Void {
      return { value, action in
        first(&value, action)
        second(&value, action)
      }
    }
    
  3. Generalize the function in the previous exercise by implementing the following variadic version:

    func combine<Value, Action>(
      _ reducers: (inout Value, Action) -> Void...
    ) -> (inout Value, Action) -> Void {
      fatalError("Unimplemented")
    }
    
    Solution
    func combine<Value, Action>(
      _ reducers: (inout Value, Action) -> Void...
    ) -> (inout Value, Action) -> Void {
      return { value, action in
        reducers.forEach { reducer in
          reducer(&value, action)
        }
      }
    }
    
  4. Break up the appReducer into 3 reducers: one for the counter view, one for the prime modal, and one for the favorites prime view. Reconstitute the appReducer by using the combine function on each of the 3 reducers you create.

    What do you lose in breaking the reducer up?

    Solution

    You can break appReducer down into the following more domain-specific reducers:

    func counterReducer(value: inout AppState, action: AppAction) -> Void {
      switch action {
      case .counter(.decrTapped):
        state.count -= 1
    
      case .counter(.incrTapped):
        state.count += 1
    
      default:
        break
      }
    }
    
    func primeModalReducer(state: inout AppState, action: AppAction) -> Void {
      switch action {
      case .primeModal(.addFavoritePrime):
        state.favoritePrimes.append(state.count)
        state.activityFeed.append(.init(timestamp: Date(), type: .addedFavoritePrime(state.count)))
    
      case .primeModal(.removeFavoritePrime):
        state.favoritePrimes.removeAll(where: { $0 == state.count })
        state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.count)))
    
      default:
        break
      }
    }
    
    func favoritePrimesReducer(state: inout AppState, action: AppAction) -> Void {
      switch action {
      case let .favoritePrimes(.removeFavoritePrimes(indexSet)):
        for index in indexSet {
          state.activityFeed.append(.init(timestamp: Date(), type: .removedFavoritePrime(state.favoritePrimes[index])))
          state.favoritePrimes.remove(at: index)
        }
    
      default:
        break
      }
    }
    

    Unfortunately, we needed to add default cases to each switch statement.

  5. Although it is nice that the previous exercise allowed us to break up the appReducer into 3 smaller ones, each of those smaller reducers still operate on the entirety of AppState, even if they only want a small piece of sub-state.

    Explore ways in which we can transform reducers that work on sub-state into reducers that work on global AppState. To get your feet wet, start by trying to implement the following function to lift a reducer on just the count field up to global state:

    func transform(
      _ localReducer: @escaping (inout Int, AppAction) -> Void
    ) -> (inout AppState, AppAction) -> Void {
      fatalError("Unimplemented")
    }
    
    Solution
    func transform(
      _ localReducer: @escaping (inout Int, AppAction) -> Void
    ) -> (inout AppState, AppAction) -> Void {
      return { appState, appAction in
        localReducer(&appState.count, appAction)
      }
    }
    
  6. Can you generalize the solution to the previous exercise to work for any generic LocalValue and GlobalValue instead of being specific to Int and AppState? And can you generalize the action to a single shared Action among global and local state?

    Hint: this solution requires you to both extract a local value from a global one to send it through the reducer, and take the updated local value and set it on the global one. Swift provides an excellent way of handling this: writable key paths!

    Solution
    func transform<GlobalValue, LocalValue, Action>(
      _ localReducer: @escaping (inout LocalValue, Action) -> Void,
      localValueKeyPath: WritableKeyPath<GlobalValue, LocalValue>
    ) -> (inout GlobalValue, Action) -> Void {
      return { globalValue, action in
        localReducer(&globalValue[keyPath: localValueKeyPath], action)
      }
    }
    

References

Reduce with inout

Chris Eidhof • Monday Jan 16, 2017

The Swift standard library comes with two versions of reduce: one that takes accumulation functions of the form (Result, Value) -> Result, and another that accumulates with functions of the form (inout Result, Value) -> Void. Both versions are equivalent, but the latter can be more efficient when reducing into large data structures.

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

Side Effects

Brandon Williams & Stephen Celis • Monday Feb 5, 2018

We first discussed the idea of equivalence between functions of the form (A) -> A and functions (inout A) -> Void in our episode on side effects. Since then we have used this equivalence many times in order to transform our code into an equivalent form while improving its performance.

Side effects: can’t live with ’em; can’t write a program without ’em. Let’s explore a few kinds of side effects we encounter every day, why they make code difficult to reason about and test, and how we can control them without losing composition.