Collection
Unlock This Episode
Our Free plan includes 1 subscriber-only episode of your choice, plus weekly updates from our newsletter.
Introduction
So creating a case path is quite easy, but there are a lot of things not completely right with this code snippet. First, it is not right to pollute the Result
namespace with these static vars, it would be better to have a different place to store them. Also, there is some boilerplate involved in creating these case paths. We see it here with this if case let
stuff when implementing the extract
, and basically every case path we create will look exactly like this, and after awhile it’s going to be a pain to have to write over and over.
We are going to solve both of those problems soon, but first we want to explore some of the properties of case paths and show the similarities with key paths. There are some operations that the Swift standard library provides for key paths, and we of course would expect that there is some version of those operations for case paths too.
Subscribe to Point-Free
Access this episode, plus all past and future episodes when you become a subscriber.
Already a subscriber? Log in
Exercises
Case paths don’t only need to focus on a single case of an enum. They can also focus on multiple cases of an enum, we just have to do a little bit of manual work first.
Consider the following enum:
enum AppAction { case activity(ActivityAction) case dashboard(DashboardAction) case profile(ProfileAction) } enum ActivityAction {} enum DashboardAction {} enum ProfileAction {}
Write a case path that can extract an
activity
orprofile
action from an app action, but not adashboard
action. Compare this to how one would write a computed property that focuses on two struct fields at the same time.Solution
To define a case path that can select several cases out of an enum, we need a type that can describe those cases. We can define it manually:
enum ActivityOrProfileAction { case activity(ActivityAction) case profile(ProfileAction }
And then we can create a case path from
AppAction
to this new container.extension CasePath where Root == AppAction, Value == ActivityOrProfile { static let activityOrProfile = CasePath( embed: { switch $0 { case let .activity(action): return .activity(action) case let .profile(action): return .profile(action) } }, extract: { switch $0 { case let .activity(action): return .activity(action) case let .profile(action): return .profile(action) default: return nil } }) }
You could even use the
Either
type for this kind of thing!CasePath<AppAction, Either<ActivityAction, ProfileAction>>
Every computed property on a type (structs, enums and classes) is given a key path for free by the Swift compiler. For example:
struct State { var count: Int var favorites: [Int] var isFavorite: Bool { get { self.favorites.contains(self.count) } set { newValue ? self.favorites.removeAll(where: { $0 == self.count }) : self.favorites.append(self.count) } } } \State.isFavorite // WritableKeyPath<State, Bool>
The
isFavorite
computed property is given aWritableKeyPath
, even though it is not a stored field on the struct.What is the equivalent concept for case paths? Theorize what a “computed case” syntax could look like in Swift.
Although enums are a great source for case paths, it is not the only situation in which case paths can occur. At its core, case paths only express the idea of being able to try to extract some data from a value, and the ability to construct a value from that data.
Implement the following case paths. A natural place to hold these case paths is as static variables on
CasePath
withRoot
andValue
suitably constrained.int: CasePath<String, Int>
uuid: CasePath<String, UUID>
literal: (String) -> CasePath<String, String>
:let blobCasePath = CasePath.literal("Blob") blob.extract("Blob") // "Blob" blob.extract("Blob Jr.") // nil blob.embed("Blob Sr.") // "Blob"
first: CasePath<[A], A>
first: (where: (A) -> Bool) -> CasePath<[A], A>
key: (K) -> CasePath<[K: V], V>
rawValue: CasePath<R.RawValue, R> where R: RawRepresentable
Solution
extension CasePath where Root == String, Value == Int { static let int = CasePath( embed: String.init, extract: Int.init ) } import Foundation extension CasePath where Root == String, Value == UUID { static let uuid = CasePath( embed: { $0.uuidString }, extract: UUID.init(uuidString:) ) } extension CasePath where Root == String, Value == String { static func literal(_ string: String) -> CasePath { CasePath( embed: { _ in string }, extract: { $0 == string ? string : nil } ) } } extension CasePath where Root: RangeReplaceableCollection, Value == Root.Element { static var first: CasePath { CasePath( embed: { Root([$0]) }, extract: { $0.first } ) } static func first( where p: @escaping (Value) -> Bool ) -> CasePath { CasePath( embed: { Root([$0]) }, extract: { $0.first(where: p) } ) } } extension CasePath { static func key<Key>( _ key: Key ) -> CasePath where Root == [Key: Value] { CasePath( embed: { [key: $0] }, extract: { $0[key] } ) } } extension CasePath where Value: RawRepresentable, Root == Value.RawValue { static var rawRepresentable: CasePath { CasePath( embed: { $0.rawValue }, extract: Value.init(rawValue:) ) } }
As we’ve seen, both key paths and case paths are composable: they both support an
appending(path:)
operation for combining key paths with key paths or case paths with case paths. The next few exercises will explore if it is possible to combine key paths with case paths in various ways.First: is it possible to implement a function that appends a key path with a case path to return a new case path?
extension _WritableKeyPath { func appending<AppendedValue>( path: CasePath<Value, AppendedValue> ) -> CasePath<Root, AppendedValue> { fatalError("unimplemented") } }
Or a function that appends a case path with a key path to return a new case path?
extension CasePath { func appending<AppendedValue>( path: _WritableKeyPath<Value, AppendedValue> ) -> CasePath<Root, AppendedValue> { fatalError("unimplemented") } }
Solution
It is not possible to implement these functions. While it is possible to define the
extract
functions, it is impossible to define theembed
functions because the key path’s root is never available.extension _WritableKeyPath { func appending<AppendedValue>( path: CasePath<Value, AppendedValue> ) -> CasePath<Root, AppendedValue> { return CasePath<Root, AppendedValue>( extract: { root in path.extract(self.get(root)) }, embed: { appendedValue in fatalError() } ) } } extension CasePath { func appending<AppendedValue>( path: _WritableKeyPath<Value, AppendedValue> ) -> CasePath<Root, AppendedValue> { CasePath<Root, AppendedValue>( extract: { root in guard let value = self.extract(root) else { return nil } return path.get(value) }, embed: { appendedValue in fatalError() }) } }
Is it possible to write a function that appends a key path with a case path to return a new key path?
extension _WritableKeyPath { func appending<AppendedValue>( path: CasePath<Value, AppendedValue> ) -> _WritableKeyPath<Root, AppendedValue> { fatalError("unimplemented") } }
How about a function that appends a case path with a key path to return a new key path?
extension CasePath { func appending<AppendedValue>( path: _WritableKeyPath<Value, AppendedValue> ) -> _WritableKeyPath<Root, AppendedValue> { fatalError("unimplemented") } }
Solution
It is not possible to implement these functions, either, because it is not possible to implement the returned key path’s getter. Each
get
function must return a non-optionalAppendedValue
, but case path extraction requires this value to be optional.extension _WritableKeyPath { func appending<AppendedValue>(path: CasePath<Value, AppendedValue>) -> _WritableKeyPath<Root, AppendedValue> { return _WritableKeyPath<Root, AppendedValue>( get: { root in let value = self.get(root) let appendedValue = path.extract(value) return appendedValue // 🛑 }, set: { root, appendedValue in self.set(&root, path.embed(appendedValue)) } ) } } extension CasePath { func appending<AppendedValue>( path: _WritableKeyPath<Value, AppendedValue> ) -> _WritableKeyPath<Root, AppendedValue> { _WritableKeyPath<Root, AppendedValue>( get: { root in guard let value = self.extract(root) else { return nil // 🛑 } let appendedValue = path.get(value) return appendedValue }, set: { root, appendedValue in guard var value = self.extract(root) else { return } path.set(&value, appendedValue) root = self.embed(value) } ) } }
Implement a function that appends a key path with a case path and returns a new key path with an optional value.
extension _WritableKeyPath { func appending<AppendedValue>( path: CasePath<Value, AppendedValue> ) -> _WritableKeyPath<Root, AppendedValue?> { fatalError("unimplemented") } }
Also implement a function that appends a case path with a key path and returns a new key path with an optional appended value.
extension CasePath { func appending<AppendedValue>( path: _WritableKeyPath<Value, AppendedValue> ) -> _WritableKeyPath<Root, AppendedValue?> { fatalError("unimplemented") } }
Solution
extension _WritableKeyPath { func appending<AppendedValue>( path: CasePath<Value, AppendedValue> ) -> _WritableKeyPath<Root, AppendedValue?> { _WritableKeyPath<Root, AppendedValue?>( get: { root in path.extract(self.get(root)) }, set: { root, appendedValue in guard let appendedValue = appendedValue else { return } self.set(&root, path.embed(appendedValue)) } ) } } extension CasePath { func appending<AppendedValue>( path: _WritableKeyPath<Value, AppendedValue> ) -> _WritableKeyPath<Root, AppendedValue?> { _WritableKeyPath<Root, AppendedValue?>( get: { root in guard let value = self.extract(root) else { return nil } return path.get(value) }, set: { root, appendedValue in guard var value = self.extract(root) else { return } guard let appendedValue = appendedValue else { return } path.set(&value, appendedValue) root = self.embed(value) } ) } }
Given the previous exercise’s operations that append key paths with case paths and case paths with key paths, what happens when you try to compose the following?
// KP = _WritableKeyPath // CP = CasePath KP<A, B> + CP<B, C> + KP<C, D>
Can key paths and case paths compose more than one time?
Solution
Because this form of composition makes the appended
Value
parameter optional, it is impossible to further append paths without introducing new operations that flatten this optional.There exists another path type that avoids the issue raised in the previous exercise. It captures the Swift semantic of optional chaining, where the getter is optional:
user?.location.city // Optional("Brooklyn")
And the setter is non-optional:
user?.location.city = "Los Angeles" // ✅ user?.location.city = nil // 🛑 error: 'nil' cannot be assigned to type 'String'
Express these requirements in a new
OptionalPath<Root, Value>
type.Solution
struct OptionalPath<Root, Value> { let extract: (Root) -> Value? let set: (inout Root, Value) -> Void }
Define
appending(path:)
onOptionalPath
:extension OptionalPath { func appending<AppendedValue>( path: OptionalPath<Value, AppendedValue> ) -> OptionalPath<Root, AppendedValue> { fatalError("unimplemented") } }
Solution
extension OptionalPath { func appending<AppendedValue>( path: OptionalPath<Value, AppendedValue> ) -> OptionalPath<Root, AppendedValue> { OptionalPath<Root, AppendedValue>( extract: { root in self.extract(root).flatMap(path.extract) }, set: { root, appendedValue in guard var value = self.extract(root) else { return } path.set(&value, appendedValue) self.set(&root, value) } ) } }
We have seen in previous episodes and exercises that key paths and case paths have an “identity” path. Define the identity optional path:
extension OptionalPath where Root == Value { static var `self`: OptionalPath { fatalError("unimplemented") } }
Solution
extension OptionalPath where Root == Value { static var `self`: OptionalPath { OptionalPath { extract: { .some($0) }, set { $0 = $1 } } } }
Implement a function that appends a key path with a case path and returns an optional path.
extension _WritableKeyPath { func appending<AppendedValue>( path: CasePath<Value, AppendedValue> ) -> OptionalPath<Root, AppendedValue> { fatalError("unimplemented") } }
Also implement a function that appends a case path with a key path and returns a new optional path.
extension CasePath { func appending<AppendedValue>( path: _WritableKeyPath<Value, AppendedValue> ) -> OptionalPath<Root, AppendedValue> { fatalError("unimplemented") } }
Solution
extension _WritableKeyPath { func appending<AppendedValue>( path: CasePath<Value, AppendedValue> ) -> OptionalPath<Root, AppendedValue> { OptionalPath<Root, AppendedValue>( extract: { root in path.extract(self.get(root)) }, set: { root, appendedValue in self.set(&root, path.embed(appendedValue)) } ) } } extension CasePath { func appending<AppendedValue>( path: _WritableKeyPath<Value, AppendedValue> ) -> OptionalPath<Root, AppendedValue> { OptionalPath<Root, AppendedValue>( extract: { root in self.extract(root).map(path.get) }, set: { root, appendedValue in guard var value = self.extract(root) else { return } path.set(&value, appendedValue) root = self.embed(value) } ) } }
Describe the hierarchy of path composition between:
KP = (Writable) Key Paths CP = Case Paths OP = Optional Paths KP + KP = ? CP + CP = ? OP + OP = ? KP + CP = ? CP + KP = ? KP + OP = ? OP + CP = ? CP + OP = ? OP + CP = ?
Solution
KP + KP = KP CP + CP = CP OP + OP = OP KP + CP = OP CP + KP = OP KP + OP = OP OP + CP = OP CP + OP = OP OP + CP = OP
With the solution to the previous exercise in hand, is it possible to reduce the number of
append
overloads you need to define between_WritableKeyPath
,CasePath
, andOptionalPath
?Solution
All compositions, whenever two paths differ, lead to
OptionalPath
. This means that_WritableKeyPath
andCasePath
are both convertible toOptionalPath
. We can employ aPath
protocol to describe this ability.protocol Path { associatedtype Root associatedtype Value var asOptionalPath: OptionalPath<Root, Value> { get } }
And all three paths can conform:
struct _WritableKeyPath: Path { var asOptionalPath: OptionalPath<Root, Value> { OptionalPath<Root, Value>( extract: { .some(self.get($0)) }, set: self.set ) } } struct CasePath: Path { var asOptionalPath: OptionalPath<Root, Value> { OptionalPath<Root, Value>( extract: self.extract, set: { $0 = self.embed($1) } ) } } struct OptionalPath: Path { var asOptionalPath: OptionalPath<Root, Value> { self } }
With these in hand, one can extend
Path
with anappending(path:)
operation:extension Path { func appending<AppendedPath: Path>( path: AppendedPath ) -> OptionalPath<Root, AppendedValue> where AppendedPath.Root == Value { self.asOptionalPath.appending(path: path.asOptionalPath) } }
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.
Getters and Key Paths
Brandon Williams & Stephen Celis • Monday Mar 19, 2018In this episode we first define the ^
operator to lift key paths to getter functions.
Key paths aren’t just for setting. They also assist in getting values inside nested structures in a composable way. This can be powerful, allowing us to make the Swift standard library more expressive with no boilerplate.
SE-0249: Key Path Expressions as Functions
Stephen Celis & Greg Titus • Tuesday Mar 19, 2019A proposal has been accepted in the Swift evolution process that would allow key paths to be automatically promoted to getter functions. This would allow using key paths in much the same way you would use functions, but perhaps more succinctly: users.map(\.name)
.
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.
How Do I Write If Case Let in Swift?
Zoë SmithThis site is a cheat sheet for if case let
syntax in Swift, which can be seriously complicated.
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.