A video series exploring functional programming and Swift.
#68 • Monday Aug 5, 2019 • Subscriber-only

Composable State Management: Reducers

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.

This episode builds on concepts introduced previously:

#68 • Monday Aug 5, 2019 • Subscriber-only

Composable State Management: Reducers

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.

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

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.

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. 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")
    }
    
  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.

  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")
    }
    
  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. Reconstitue the appReducer by using the combine function on each of the 3 reducers you create.

  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")
    }
    

    Can you generalize this to work for any generics LocalValue and GlobalValue instaed of being specific to Int and AppState?


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.

Chapters
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