Case paths bring the power and ergonomics of key paths to enums.
Swift endows every struct and class property with a key path.
struct User {
let id: Int
var name: String
}
\User.id // KeyPath<User, Int>
\User.name // WritableKeyPath<User, String>
This is compiler-generated code that can be used to abstractly zoom in on part of a structure, inspect and even change it, all while propagating those changes to the structure's whole. They are the silent partner of many modern Swift APIs powered by dynamic member lookup, like SwiftUI bindings, but also make more direct appearances, like in the SwiftUI environment.
Unfortunately, no such structure exists for enum cases!
enum UserAction {
case home(HomeAction)
case settings(SettingsAction)
}
\UserAction.settings // 🛑
And so it's impossible to write generic code that can zoom in on and propagate changes to a particular case.
This library intends to bridge this gap by introducing what we call "case paths."
Case paths can be enabled for an enum using the @CasePathable
macro:
@CasePathable
enum UserAction {
case home(HomeAction)
case settings(SettingsAction)
}
And case paths can be produced from a "case-pathable" enum using the #casePath
macro:
#casePath(\UserAction.home) // Case<UserAction, HomeAction>
#casePath(\UserAction.settings) // Case<UserAction, SettingsAction>
While key paths package up the functionality of getting and setting a value on a root structure, case paths package up the functionality of extracting and embedding a value on a root enumeration.
user[keyPath: \.name] += ", Jr."
user[keyPath: \.name] // "Blob, Jr."
let action = #casePath(\UserAction.home).embed(.onAppear)
#casePath(\.home).extract(from: action) // Optional(HomeAction.onAppear)
Case path extraction can fail and return nil
because the cases may not match up.
#casePath(\.settings).extract(from: action) // nil
Case paths, like key paths, compose. Where key paths use dot-syntax to dive deeper into a structure, case paths use optional-chaining:
\HighScore.user.name
// WritableKeyPath<HighScore, String>
#casePath(\UserAction.home?.timeline)
// Case<UserAction, TimelineAction>
Case paths, also like key paths, provide an "identity" path, which is useful for interacting with APIs that use key paths and case paths but you want to work with entire structure.
\User.self // WritableKeyPath<User, User>
#casePath(\UserAction.self) // Case<UserAction, UserAction>
If you want to discuss this library or have a question about how to use it to solve a particular problem, there are a number of places you can discuss with fellow Point-Free enthusiasts:
- For long-form discussions, we recommend the discussions tab of this repo.
- For casual chat, we recommend the Point-Free Community Slack.
The latest documentation for CasePaths' APIs is available here.
EnumKit
is a protocol-oriented, reflection-based solution to ergonomic enum access and inspired the creation of this library.
These concepts (and more) are explored thoroughly in Point-Free, a video series exploring functional programming and Swift hosted by Brandon Williams and Stephen Celis.
The original design of this library was explored in the following Point-Free episodes:
- Episode 87: The Case for Case Paths: Introduction
- Episode 88: The Case for Case Paths: Properties
- Episode 89: Case Paths for Free
All modules are released under the MIT license. See LICENSE for details.