A video series exploring functional programming and Swift.
#89 • Monday Feb 3, 2020 • Subscriber-only

Case Paths for Free

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.

This episode builds on concepts introduced previously:

#89 • Monday Feb 3, 2020 • Subscriber-only

Case Paths for Free

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.

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

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.

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

Chapters
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