Case Paths for Free

Episode #89 • Feb 3, 2020 • Subscriber-Only

Although case paths are powerful and a natural extension of key paths, they are difficult to work with right now. They require either hand-written boilerplate, or code generation. However, there’s another way to generate case paths for free, and it will make them just as ergonomic to use as key paths.

Case Paths for Free
Introduction
00:05
Reflection in Swift
02:20
Reflecting into enums
04:26
Reflecting case paths
10:55
Reflection gotchas
14:28
Introducing the / operator
18:48
What’s the point?
23:44

Unlock This Episode

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

Introduction

However, something can even be done about that boilerplate. As we have seen, the boilerplate in creating case paths has to do with the extract function, which tries to extract an associated value from an enum. The embed function comes for free in Swift because each case of an enum acts as a function that can embed the associated value into the enum, but the extract takes some work.

One way to try to get rid of this boilerplate is to turn to code generation. In fact, this is what we did in our episode on enum properties, where we showed how we could give enums an API to access the data it held that had the same ergonomics as simple dot syntax that structs can use. The API came with some boilerplate, and so we then a tool over a few episodes (part 1, part 2, part 3) that uses Swift Syntax to analyze our source code and automatically generate the enum properties for all of the enums in our project.

That was really powerful, and we could maybe turn to code generation for case paths, but also code generation is quite heavy. We need to find the best way to run the tool whenever source code changes to make sure it’s up to date, and that can complicate the build process.

It turns out, for case paths in particular we can do something different. We can magically derive the extract function for a case path from just the embed function that comes from the case of an enum. We say this is “magical” because it uses Swift’s runtime reflection capabilities.

If you are not familiar with the idea of reflection in programming, all you need to know is it allows you to inspect the internal structure of values and objects at runtime. For example, you can use reflection to get a list of all of the string names of stored properties that a struct has.

Any time you use reflection in Swift you are purposely going outside the purview of the Swift compiler. This means you are in dangerous waters since the compiler doesn’t have your back. However, if you tread lightly and write lots of tests, you can come up with something somewhat reasonable that can clear away all of the repetitive boilerplate. Let’s start by exploring the reflection API a bit and see what is available to us.

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 the Composable Architecture we have been building, the pullback operation on reducers took two transformations: a writable key path for state and a writable key path for actions. Replace the key path on actions with a case path, and update the project to use this new API.

    Solution
    public func pullback<LocalValue, GlobalValue, LocalAction, GlobalAction>(
      _ reducer: @escaping Reducer<LocalValue, LocalAction>,
      value: WritableKeyPath<GlobalValue, LocalValue>,
      action: CasePath<GlobalAction, LocalAction>
    ) -> Reducer<GlobalValue, GlobalAction> {
      return { globalValue, globalAction in
        guard let localAction = action.extract(globalAction) else { return [] }
        let localEffects = reducer(&globalValue[keyPath: value], localAction)
    
        return localEffects.map { localEffect in
          localEffect.map(action.embed).eraseToEffect()
        }
      }
    }
    
  2. Our reflection-based case path initializer is relatively simple, but it’s also relatively brittle: there are several edge cases that it will fail to work with. The next few exercises will explore finding solutions to these edge cases.

    For one, given the following enum with a labeled case.

    enum EnumWithLabeledCase {
      case labeled(label: Int)
    }
    

    Extraction fails:

    extractHelp(
      case: EnumWithLabeledCase.labeled,
      from: EnumWithLabeledCase.labeled(label: 1)
    )
    // nil
    

    Study the mirror of EnumWithLabeledCase.labeled(label: 1) and update extractHelp with the capability of extracting this value.

    Note that it is perfectly valid Swift to add a second case to this enum with the same name but a different label:

    enum EnumWithLabeledCase {
      case labeled(label: Int)
      case labeled(anotherLabel: Int)
    }
    

    Ensure that extractHelp fails to extract this mismatch:

    Solution

    EnumWithLabeledCase.labeled(label: 1) has a single child with an unusual value:

    let labeled = EnumWithLabeledCase.labeled(label: 1)
    let children = Mirror(reflecting: labeled).children
    children.count  // 1
    children.first! // (label: "labeled", value: (label: 1))
    

    Strange! The Mirror.Child has a label matching the case name, but its value appears to be a tuple of one labeled element, something that’s typically forbidden in Swift. It’s important to note that the tuple label matches the associated value’s label.

    In order to interact with this structure, we must again reflect.

    let newChildren = Mirror(reflecting: children.first!.value)
    newChildren.count  // 1
    newChildren.first! // (label: "label", value: 1)
    

    Alright, this should be enough for us to update extractHelp to do just a little more work. We want to dive one step further into the structure, so let’s extract that work into a helper function in order to perform it twice.

    func extractHelp<Root, Value>(
      case: @escaping (Value) -> Root,
      from root: Root
    ) -> Value? {
      func reflect(_ root: Root) -> ([String?], Value)? {
        let mirror = Mirror(reflecting: root)
        guard let child = mirror.children.first else { return nil }
        if let value = child.value as? Value { return ([child.label], value) }
    
        let newMirror = Mirror(reflecting: child.value)
        guard let newChild = newMirror.children.first else { return nil }
        if let value = newChild.value as? Value { return ([child.label, newChild.label], value) }
    
        return nil
      }
    
      guard let (path, value) = reflect(root) else { return nil }
      guard let (newPath, _) = reflect(`case`(value)) else { return nil }
      guard path == newPath else { return nil }
    
      return value
    }
    

References

Extract Payload for enum cases having associated value

Giuseppe Lanza • Sunday Aug 4, 2019

This Swift forum post offers a reflection-based solution for extracting an enum’s associated value and inspired our solution for deriving case paths from enum case embed functions.

EnumKit

Giuseppe Lanza

A protocol-oriented library for extracting an enum’s associated value.

Reflectable enums in Swift 3

Josh Smith • Saturday Apr 8, 2017

An early exploration of how an enum’s associated values can be extracted using reflection and the case name.

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.