A video series exploring functional programming and Swift.
#87 • Monday Jan 20, 2020 • Subscriber-only

The Case for Case Paths: Introduction

You’ve heard of key paths, but…case paths!? Today we introduce the concept of “case paths,” a tool that helps you generically pick apart an enum just like key paths allow you to do for structs. It’s the tool you never knew you needed.

#87 • Monday Jan 20, 2020 • Subscriber-only

The Case for Case Paths: Introduction

You’ve heard of key paths, but…case paths!? Today we introduce the concept of “case paths,” a tool that helps you generically pick apart an enum just like key paths allow you to do for structs. It’s the tool you never knew you needed.


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

We have explored the idea that Swift’s structs and enums are intimately and fundamentally related to each other in many past Point-Free episodes. We first did this in our episodes on algebraic data types, where we showed that in a very precise sense enums correspond to addition that we all know and love from algebra, and structs correspond to multiplication. We even showed that certain algebraic properties also carry over to Swift, like the fact that multiplication distributes over addition means that you can refactor your own data types to factor out common pieces of data.

We took this idea even further in another episode where we claimed that literally any feature you endow structs with there should be a corresponding version of it for enums, and vice versa. There are a few examples of where this principle holds, or mostly holds, for various features of structs and enums. However, in general this is not the case, and there are many instances where Swift clearly favors structs over enums where Swift gives structs many features that enums do not get.

One particular example we looked at was how ergonomic it is to access the data inside a struct. One can simply use dot syntax to get and set any field inside a struct value. The same is not true of enums, you have no choice but to do a switch, if case let or guard case let, which are syntactically heavy when all you wanna do is access a little bit of data inside an enum.

This led us down the path of defining what we called “enum properties”, which give enums the power of accessing their data with simple dot-syntax just like structs. We even created a code generation tool (part 1, part 2, part 3), using Apple’s Swift Syntax library, to automatically generate these properties for us to make it as easy as possible.

We want to take that work even further and discuss another feature that Swift structs are blessed with but which sadly has no representation for enums, and that is key paths. Key paths are a powerful feature in Swift, and we’ve talked about and used them a ton on Point-Free. Every field on a struct magically gets a key path generated for it by the compiler. But what does the corresponding concept look like when applied to enums? Answering this question will give us a tool for unlocking even more power from enums, and will even help us greatly simplify some code we previously wrote for the composable architecture.

Let’s start by reminding ourselves what key paths are and why they are so powerful.

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. Define the “never” case path: for any type A there exists a unique case path CasePath<A, Never>.

    This operation is useful for when you don’t want to focus on any part of the type.

    Solution

    This solution depends on the absurd function, which we explored way back in one of our episodes on algebraic data types. It can be defined as the following:

    func absurd<A>(_ never: Never) -> A {}
    

    With it in hand, we can pass it to the embed part of the case path, whereas extract expects us to return an optional Never. Values of Never are impossible to construct, so we’re left with no other choice but to return nil.

    extension CasePath where Value == Never {
      static var never: CasePath {
        CasePath(
          extract: { _ in nil },
          embed: absurd
        )
      }
    }
    
  2. Define the “void” key path: for any type A there is a unique key path _WritableKeyPath<A, Void>.

    This operation is useful for when you do not want to focus on any part of the type.

    Define this operation from scratch on the _WritableKeyPath type:

    struct _WritableKeyPath<Root, Value> {
      let get: (Root) -> Value
      let set: (inout Root, Value) -> Void
    }
    

    Is it possible to define this key path on Swift’s WritableKeyPath?

    Solution
    extension _WritableKeyPath where Value == Void {
      static var void: _WritableKeyPath {
        _WritableKeyPath(
          get: { _ in () },
          set: { _, _ in }
        )
      }
    }
    

    It is possible to define a “void” key path on a specific type or protocol by defining a “void” property:

    protocol Voidable {}
    
    extension Voidable {
      var void: Void {
        get { () }
        set {}
      }
    }
    
    struct Foo: Voidable {}
    
    \Foo.void // WritableKeyPath<Foo, Void>
    

    But it is not currently possible to define a general void key path because key paths are not transformable, and Any is not extensible.

  3. Key paths are equipped with an operation that allows you to append them. For example:

    struct Location {
      var name: String
    }
    struct User {
      var location: Location
    }
    
    (\User.location).appending(path: \Location.name)
    // WritableKeyPath<User, String>
    

    Define appending(path:) from scratch on _WritableKeyPath.

    Solution
    extension _WritableKeyPath {
      func appending<AppendedValue>(path: _WritableKeyPath<Value, AppendedValue>) -> _WritableKeyPath<Root, AppendedValue> {
        return _WritableKeyPath<Root, AppendedValue>(
          get: { root in path.get(self.get(root)) },
          set: { root, appendedValue in
            var value = self.get(root)
            path.set(&value, appendedValue)
            self.set(&root, value)
        })
      }
    }
    
  4. Define an appending(path:) method on CasePath, which allows you to combine a CasePath<A, B> and a CasePath<B, C>, into a CasePath<A, C>.

    Solution
    extension CasePath {
      func appending<AppendedValue>(
        path: CasePath<Value, AppendedValue>
      ) -> CasePath<Root, AppendedValue> {
        CasePath<Root, AppendedValue>(
          extract: { root in
            self.extract(root).flatMap(path.extract)
        },
          embed: { appendedValue in
            self.embed(path.embed(appendedValue))
        })
      }
    }
    
  5. Every type in Swift automatically comes with a special key path known as the “identity” key path. One gets access to it with the following syntax:

    \User.self
    \Int.self
    \String.self
    

    Define this operation for _WritableKeyPath.

    Solution
    extension _WritableKeyPath where Root == Value {
      static var `self`: _WritableKeyPath {
        _WritableKeyPath(
          get: { $0 },
          set: { root, value in root = value }
        )
      }
    }
    
  6. Define the “self” case path: for any type A there is a case path CasePath<A, A>.

    This case path is useful for when you want to focus on the whole type.

    Solution
    extension CasePath where Root == Value {
      static var `self`: CasePath {
        CasePath(
          extract: { .some($0) },
          embed: { $0 }
        )
      }
    }
    
  7. Implement the “pair” key path: for any types A, B, and C one can combine two key paths _WritableKeyPath<A, B> and _WritableKeyPath<A, C> into a third key path _WritableKeyPath<A, (B, C)>.

    This operation allows you to easily focus on two properties of a struct at once.

    Note that this is not possible to do with Swift’s WritableKeyPath because they are not directly constructible by us, only by the compiler.

    Solution
    func pair<A, B, C>(
      _ lhs: _WritableKeyPath<A, B>,
      _ rhs: _WritableKeyPath<A, C>
    ) -> _WritableKeyPath<A, (B, C)> {
      _WritableKeyPath(
        get: { a in (lhs.get(a), rhs.get(a)) },
        set: { a, bc in
          lhs.set(a, bc.0)
          rhs.set(a, bc.1)
      })
    }
    
  8. Implement the “either” case path: for any types A, B and C one can combine two case paths CasePath<A, B>, CasePath<A, C> into a third case path CasePath<A, Either<B, C>>, where:

    enum Either<A, B> {
      case left(A)
      case right(B)
    }
    

    This operation allows you to easily focus on two cases of an enum at once.

    Solution
    func either<A, B, C>(
      _ lhs: CasePath<A, B>,
      _ rhs: CasePath<A, C>
    ) -> CasePath<A, (B, C)> {
      CasePath<A, Either<B, C>>(
        extract: { a in
          lhs.extract(a).map(Either.left)
            ?? rhs.extract(a).map(Either.right)
        },
        embed: { bOrC in
          switch bOrC {
          case let .left(b):  lhs.embed(b)
          case let .right(c): rhs.embed(c)
          }
        })
    }
    

References

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

  • Swift Tip: Bindings with KVO and Key Paths

    Chris Eidhof & Florian Kugler • Tuesday Apr 24, 2018

    This handy Swift tip shows you how to create bindings between object values using key paths, similar to the helper we used in this episode.

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

Chapters
Introduction
00:05
Key paths: a refresher
02:35
Key paths in practice: bindings
05:02
Key paths in practice: reducers
10:40
Introducing: case paths
20:18
Defining a couple case paths
25:09
Next time: the properties of case paths
28:31