Composing Architecture with Case Paths

Episode #90 • Feb 10, 2020 • Subscriber-Only

Let’s explore a real world application of “case paths,” which provide key path-like functionality to enum cases. We’ll upgrade our composable architecture to use them and see why they’re a better fit than our existing approach.

Previous episode
Composing Architecture with Case Paths
Introduction
00:05
Refresher: the Composable Architecture
02:32
The problem with enum properties
04:39
Case paths in the architecture
13:58
Case paths in the application
17:18
What's the point?
20:48

Unlock This Episode

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

Introduction

Last week we concluded an introduction to the concept of “case paths”. It’s a tool that lets you generically isolate a single case of an enum from the rest of the cases. We were inspired to create this tool because we saw just how handy key paths are, and how they allow us to write generic algorithms that can pick apart and reassemble structs, and it stands to reason that such a thing would be really nice for enums too.

And in fact, we have a great use case for that already in the Composable Architecture. When we wanted to modularize our reducers, so that a reducer could work on just the domain it cared about while still allowing itself to be plugged into the global domain, we were naturally led to the pullback operation. It lets you take a reducer that works on a local domain and pull it back to work on the global domain. And then you can take a whole bunch of these little reducers and combine them together into one big ole reducer that powers your entire application.

In order to define this pullback operation we used key paths, and it was a great example of how you can write generic algorithms that abstract over the shape of data. We could use the key paths to pluck out the pieces of a data structure we care about, run the reducer on those smaller parts, and then use the key path again to glue everything back together.

However, our usage of key paths for the action of the reducer was a little strange. Perhaps the most glaring issue was that we had to turn to code generation to even get access to the key path. Fortunately, all of the work we did for case paths is applicable to the Composable Architecture, and it will simultaneously simplify our pullback operation and get rid of all of the code generation.

It may sound too good to be true, but it is true. But before we get to that, let’s take a quick trip down memory lane to see what our app looks like and a brief overview of how we built it.

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 week’s episode let’s explore more real world applications of case paths.

    SwiftUI introduces a Binding type, which is a getter-setter pair that isn’t so different from a writable key path, and in fact has an operation that takes a key path from the binding’s root to a value that returns a new binding for the value.

    Is it possible to define this operation for case paths?

    extension Binding {
      subscript<Subject>(
        casePath: CasePath<Value, Subject>
      ) -> Binding<Subject> {
        fatalError("unimplemented")
      }
    }
    
    Solution

    Any implementation of this function will be unsafe: case path extraction results in an optionally failable operation, which means the get function of the binding would need to force unwrap the value,

  2. Implement the following function, which is similar to the previous exercise, but optionalizes the returned binding.

    extension Binding {
      subscript<Subject>(
        casePath: CasePath<Value, Subject>
      ) -> Binding<Subject>? {
        fatalError("unimplemented")
      }
    }
    
    Solution
    extension Binding {
      subscript<Subject>(
        casePath: CasePath<Value, Subject>
      ) -> Binding<Subject>? {
        casePath.extract(from: self.wrappedValue).map { subject in
          Binding<Subject>(
            get: { subject },
            set: { self.wrappedValue = casePath.embed($0) }
          )
        }
      }
    }
    
  3. While we typically model application state using structs, it is totally valid to model application state using enums. For example, your root state may be an enumeration of logged-in and logged-out states:

    enum AppState {
      case loggedIn(LoggedInState)
      case loggedOut(LoggedOutState)
    }
    

    Use the case path helper from the previous exercise and define a root AppView that works with AppState and renders a LoggedInView or LoggedOutView with a Binding for their state.

    Solution
    struct LoggedInView: View {
      @Binding var state: LoggedInState
    
      var body: some View {
        EmptyView()
      }
    }
    
    struct LoggedOutView: View {
      @Binding var state: LoggedOutState
    
      var body: some View {
        EmptyView()
      }
    }
    
    struct AppView: View {
      @Binding var state: AppState
    
      var body: some View {
        if let loggedInBinding = self.$state[/AppState.loggedIn] {
          return AnyView(LoggedInView(state: loggedInBinding))
        } else if let loggedOutBinding = self.$state[/AppState.loggedOut] {
          return AnyView(LoggedOutView(state: loggedOutBinding))
        } else {
          return AnyView(EmptyView())
        }
      }
    }
    
  4. The previous exercises introduce a case path helper analog to a key path helper that uses dynamic member lookup. Theorize how a first-class case path dynamic member lookup API would look like in Swift.

  5. Combine defines a key path API for reactive bindings:

    extension Publisher where Failure == Never {
      func assign<Root>(
        to keyPath: ReferenceWritableKeyPath<Root, Output>,
        on object: Root
      ) -> AnyCancellable
    }
    

    What would it take to define an equivalent API that takes case paths?

    Solution

    This API depends on a “reference-writable” key path in order to capture the reference to an object to be mutated later. Case paths, however, typically describe enums, which are value types, which cannot be mutated in the same way. For a function to mutate a value, the value must be annotated with inout and this mutable value cannot be captured for mutation later.

  6. Swift defines a key path API for key-value observing:

    extension NSObject {
      func observe<Value>(
        keyPath: KeyPath<Self, Value>,
        options: NSKeyValueObservingOptions,
        changeHandler: @escaping (Self, NSKeyValueObservedChange<Value>) -> Void
      )
    }
    

    What would it take to define an equivalent API that takes case paths?

    Solution

    Swift’s key-value observing APIs depends on the Objective-C runtime, and the method that takes key paths is based on a more primitive method that uses the property’s name to observe changes to the property. Swift enums are not portable to Objective-C, so no such API can exist today.

  7. Take additional, existing APIs that take key paths and explore their case path equivalents! And share them with us!

References

CasePaths

Brandon Williams & Stephen Celis

CasePaths is one of our open source projects for bringing the power and ergonomics of key paths to enums.

Structs 🤝 Enums

Brandon Williams & Stephen Celis • Monday Mar 25, 2019

In this episode we explore the duality of structs and enums and show that even though structs are typically endowed with features absent in enums, we can often recover these imbalances by exploring the corresponding notion.

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.

Make your own code formatter in Swift

Yasuhiro Inami • Saturday Jan 19, 2019

Inami uses the concept of case paths (though he calls them prisms!) to demonstrate how to traverse and focus on various parts of a Swift syntax tree in order to rewrite it.

Code formatter is one of the most important tool to write a beautiful Swift code. If you are working with the team, ‘code consistency’ is always a problem, and your team’s guideline and code review can probably ease a little. Since Xcode doesn’t fully fix our problems, now it’s a time to make our own automatic style-rule! In this talk, we will look into how Swift language forms a formal grammar and AST, how it can be parsed, and we will see the power of SwiftSyntax and it’s structured editing that everyone can practice.

Introduction to Optics: Lenses and Prisms

Giulio Canti • Thursday Dec 8, 2016

Swift’s key paths appear more generally in other languages in the form of “lenses”: a composable pair of getter/setter functions. Our case paths are correspondingly called “prisms”: a pair of functions that can attempt to extract a value, or embed it. In this article Giulio Canti introduces these concepts in JavaScript.

Optics By Example: Functional Lenses in Haskell

Chris Penner

Key paths and case paths are sometimes called lenses and prisms, but there are many more flavors of “optics” out there. Chris Penner explores many of them in this book.