Skip to content

pointfreeco/swift-case-paths

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🧰 CasePaths

CI Slack

Case paths bring the power and ergonomics of key paths to enums.

Motivation

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.

Introducing: case paths

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>

Case paths vs. key paths

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>

Community

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:

Documentation

The latest documentation for CasePaths' APIs is available here.

Other libraries

  • EnumKit is a protocol-oriented, reflection-based solution to ergonomic enum access and inspired the creation of this library.

Interested in learning more?

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:

video poster image

License

All modules are released under the MIT license. See LICENSE for details.

About

🧰 Case paths extends the key path hierarchy to enum cases.

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

Contributors 33