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.
Subscribe to Point-Free
Access this episode, plus all past and future episodes when you become a subscriber.
Already a subscriber? Log in
Exercises
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() } } }
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 updateextractHelp
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, 2019This 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 LanzaA protocol-oriented library for extracting an enum’s associated value.
Reflectable enums in Swift 3
Josh Smith • Saturday Apr 8, 2017An 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, 2019In 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, 2019Inami 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, 2016Swift’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 PennerKey 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.