Collection
Unlock This Episode
Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.
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
Access this episode, plus all past and future episodes when you become a subscriber.
Already a subscriber? Log in
Exercises
Define the “never” case path: for any type
A
there exists a unique case pathCasePath<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, whereasextract
expects us to return an optionalNever
. Values ofNever
are impossible to construct, so we’re left with no other choice but to returnnil
.extension CasePath where Value == Never { static var never: CasePath { CasePath( extract: { _ in nil }, embed: absurd ) } }
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.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) }) } }
Define an
appending(path:)
method onCasePath
, which allows you to combine aCasePath<A, B>
and aCasePath<B, C>
, into aCasePath<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)) }) } }
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 } ) } }
Define the “self” case path: for any type
A
there is a case pathCasePath<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 } ) } }
Implement the “pair” key path: for any types
A
,B
, andC
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) }) }
Implement the “either” case path: for any types
A
,B
andC
one can combine two case pathsCasePath<A, B>
,CasePath<A, C>
into a third case pathCasePath<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, 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.
Swift Tip: Bindings with KVO and Key Paths
Chris Eidhof & Florian Kugler • Tuesday Apr 24, 2018This 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, 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.