Composable State Management: Action Pullbacks

Episode #70 • Aug 19, 2019 • Subscriber-Only

Turns out, reducers that work on local actions can be pulled back to work on global actions. However, due to an imbalance in how Swift treats enums versus structs it takes a little work to implement. But never fear, a little help from our old friends “enum properties” will carry us a long way.

Action Pullbacks
Introduction
00:05
Focusing a reducer's actions
01:14
Enums and key paths
03:40
Enum properties
08:58
Pulling back reducers along actions
15:16
Pulling back more reducers
21:40
Till next time
26:26

Unlock This Episode

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

Introduction

So we are now getting pretty close to accomplishing yet another architectural problem that we set out to solve at the beginning of this series of episodes. We stated that we wanted to be able to build large complex applications out of simple, composable units.

We can now do this with our reducers and the state they operate on. We can write our reducers so that they operate on just the bare minimum of state necessary to get the job done, and then pull them back to fit inside a reducer that is much larger and operates on a full application’s state.

Ideally we’d even want those simple, composable units to be so isolated that we may even be able to put them in their own module so that they could easily be shared with other modules and apps.

This is getting pretty exciting! But, there’s still a problem. Even though our reducers are operating on smaller pieces of data, they still know far too much about the larger reducer they are embedded in, particularly they can listen in on every single app action.

It sounds like we need to repeat the same story for actions that we have for state.

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. We’ve seen that it is possible to pullback reducers along action key paths, but could we have also gone the other direction? That is, can we define a map with key paths too? If this were possible, then we could implement the following signature:

    func map<Value, Action, OtherAction>(
      _ reducer: @escaping (inout Value, Action) -> Void,
      value: WritableKeyPath<Action, OtherAction>
    ) -> (inout Value, OtherAction) -> Void {
      fatalError("Unimplemented")
    }
    

    Can this function be implemented? If not, what goes wrong?

    Solution

    This function cannot be implemented. As we saw last time with value key paths, if we try to implement this function we quickly hit a roadblock:

    func map<Value, Action, OtherAction>(
      _ reducer: @escaping (inout Value, Action) -> Void,
      value: WritableKeyPath<Action, OtherAction>
    ) -> (inout Value, OtherAction) -> Void {
      return { value, otherAction in
    
      }
    }
    

    We have a local OtherAction that we can pluck out of a global Action, but the reducer we have requires a global Action, which we have no access to.

  2. Right now we have activity feed logic scattered throughout a few reducers, such as our primeModalReducer and favoritePrimesReducer. The mutations we perform for the activity feed are independent of the other logic going on in those reducers, which means it’s ripe for extracting in some way.

    Explore how one can extract all of the activity feed logic out of our reducers by transforming our appReducer into a whole new reducer, and inside that transformation one would perform all of the activity feed logic. Such a transformation would have the following signature:

    func activityFeed(
      _ reducer: @escaping (inout AppState, AppAction) -> Void
    ) -> (inout AppState, AppAction) -> Void {
      fatalError("Unimplemented activity feed logic")
    }
    

    You would apply this function to the appReducer to obtain a whole new reducer that has the activity feed logic baked in, without needing to add anything to the reducers that make up appReducer.

    Solution
    func activityFeed(
      _ reducer: @escaping (inout AppState, AppAction) -> Void
    ) -> (inout AppState, AppAction) -> Void {
    
      return { state, action in
        switch action {
        case .counter:
          break
    
        case .primeModal(.removeFavoritePrimeTapped):
          state.activityFeed.append(
            .init(timestamp: Date(), type: .removedFavoritePrime(state.count))
          )
    
        case .primeModal(.addFavoritePrime):
          state.activityFeed.append(
            .init(timestamp: Date(), type: .saveFavoritePrimeTapped(state.count))
          )
    
        case let .favoritePrimes(.deleteFavoritePrimes(indexSet)):
          for index in indexSet {
            state.activityFeed.append(
              .init(timestamp: Date(), type: .removedFavoritePrime(state.favoritePrimes[index]))
            )
          }
        }
    
        reducer(&state, action)
      }
    }
    
    activityFeed(appReducer)
    
  3. Explore ways of adding logging to our application. Perhaps the easiest is to add print statements to the send action of our Store. That would allow you to get logging for every single action sent to the store, and you can log the state that resulted from that mutation.

    However, there is a nicer way of adding logging to our application. Instead of putting it in the Store, where not all users of the Store class may want logging, try implementing a transformation of reducer functions that automatically adds logging to any reducer.

    Such a function would have the following signature:

    func logging<Value, Action>(
      _ reducer: @escaping (inout Value, Action) -> Void
    ) -> (inout Value, Action) -> Void
    

    You would apply this function to the appReducer to obtain a whole new reducer that logs whenever an action is processed by the reducer.

    Are there any similarities to this transformation and the transformation from the previous exercise?

    Solution
    func logging(
      _ reducer: @escaping (inout AppState, AppAction) -> Void
    ) -> (inout AppState, AppAction) -> Void {
      return { value, action in
        reducer(&value, action)
        print("Action: \(action)")
        print("State:")
        dump(value)
        print("---")
      }
    }
    

References

Contravariance

Brandon Williams & Stephen Celis • Monday Apr 30, 2018

We first explored the concept of the pullback in our episode on “contravariance”, although back then we used a different name for the operation. The pullback is an instrumental form of composition that arises in certain situations, and can often be counter-intuitive at first sight.

Let’s explore a type of composition that defies our intuitions. It appears to go in the opposite direction than we are used to. We’ll show that this composition is completely natural, hiding right in plain sight, and in fact related to the Liskov Substitution Principle.

Category Theory

The topic of category theory in mathematics formalizes the idea we were grasping at in this episode where we claim that pulling back along key paths is a perfectly legimate thing to do, and not at all an abuse of the concept of pullbacks. In category theory one fully generalizes the concept of a function that maps values to values to the concept of a “morphism”, which is an abstract process that satisfies some properties with respect to identities and composition. Key paths are a perfectly nice example of morphisms, and so category theory is what gives us the courage to extend our usage of pullbacks to key paths.

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

Structs 🤝 Enums

Brandon Williams & Stephen Celis • Monday Mar 25, 2019

To understand why it is so important for Swift to treat structs and enums fairly, look no further than our episode on the topic. In this episode we demonstrate how many features of one manifest themselves in the other naturally, yet there are still some ways in which Swift favors structs over enums.

Name a more iconic duo… We’ll wait. Structs and enums go together like peanut butter and jelly, or multiplication and addition. One’s no more important than the other: they’re completely complementary. This week we’ll explore how features on one may surprisingly manifest themselves on the other.

Enum Properties

Brandon Williams & Stephen Celis • Monday Apr 1, 2019

The concept of “enum properties” were essential for our implementation of the “action pullback” operation on reducers. We first explored this concept in episode #52 and showed how this small amount of boilerplate can improve the ergonomics of data access in enums.

Swift makes it easy for us to access the data inside a struct via dot-syntax and key-paths, but enums are provided no such affordances. This week we correct that deficiency by defining the concept of “enum properties”, which will give us an expressive way to dive deep into the data inside our enums.

Swift Syntax Command Line Tool

Brandon Williams & Stephen Celis • Monday Apr 22, 2019

Although “enum properties” are powerful, it is a fair amount of boilerplate to maintain if you have lots of enums. Luckily we also were able to create a CLI tool that can automate the process! We use Apple’s SwiftSyntax library to edit source code files directly to fill in these important properties.

Today we finally extract our enum property code generator to a Swift Package Manager library and CLI tool. We’ll also do some next-level snapshot testing: not only will we snapshot-test our generated code, but we’ll leverage the Swift compiler to verify that our snapshot builds.

pointfreeco/swift-enum-properties

Brandon Williams & Stephen Celis • Monday Apr 29, 2019

Our open source tool for generating enum properties for any enum in your code base.

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.

Pullback

We use the term pullback for the strange, unintuitive backwards composition that seems to show up often in programming. The term comes from a very precise concept in mathematics. Here is the Wikipedia entry:

In mathematics, a pullback is either of two different, but related processes: precomposition and fibre-product. Its “dual” is a pushforward.

Some news about contramap

Brandon Williams • Monday Oct 29, 2018

A few months after releasing our episode on Contravariance we decided to rename this fundamental operation. The new name is more friendly, has a long history in mathematics, and provides some nice intuitions when dealing with such a counterintuitive idea.