# CasePath
## 🔗 GitHub
- https://github.com/pointfreeco/swift-case-paths
## 🎬 Point・Free
- [https://www.pointfree.co/episodes/ep87-the-case-for-case-paths-introduction](https://www.pointfree.co/episodes/ep87-the-case-for-case-paths-introduction)
- [https://www.pointfree.co/episodes/ep88-the-case-for-case-paths-properties](https://www.pointfree.co/episodes/ep88-the-case-for-case-paths-properties)
- [https://www.pointfree.co/episodes/ep89-case-paths-for-free](https://www.pointfree.co/episodes/ep89-case-paths-for-free)
## Content
1. KeyPath
2. What is a KeyPath used for?
3. CasePath
4. Key points
5. Supplement
## KeyPath
> A key path from a specific root type to a specific resulting value type.
>
我們通常會這樣寫...
```swift=
class Location {
var city: String
var country: String
init(city: String, country: String) {
self.city = city
self.country = country
}
}
struct User {
let id: Int
let name: String
let age: Int
var phone: String?
var location: Location
var isAdult: Bool {
age >= 18
}
}
let location = Location(city: "Taipei", country: "Taiwan")
let user = User(id: 1, name: "Bing", age: 28, phone: "0900000000", location: location)
user.name // "Bing"
user.location.country // "Taiwan"
```
KeyPath 寫法會這樣寫...
```swift=
let userPath = \User.name // KeyPath<User, String>
user[keyPath: userPath] // "Bing"
let phonePath = \User.phone // WritableKeyPath<User, Optional<String>>
user[keyPath: phonePath] // "0900000000"
let locationPath = \User.location.city // ReferenceWritableKeyPath<User, String>
user[keyPath: locationPath] // Taipei
user[keyPath: \.location.city] // type inference
```
**概念**上像這樣,有一組 getter & setter
```swift=
struct _WritableKeyPath<Root, Value> {
let get: (Root) -> Value
let set: (inout Root, Value) -> Void
}
```
## What is a KeyPath used for?
> They unlock the ability to do many things, like [key-value observing](https://developer.apple.com/documentation/swift/cocoa_design_patterns/using_key-value_observing_in_swift) and [reactive bindings](https://developer.apple.com/documentation/combine/receiving_and_handling_events_with_combine), [dynamic member lookup](https://github.com/apple/swift-evolution/blob/master/proposals/0252-keypath-dynamic-member-lookup.md), and scoping changes to the SwiftUI [environment](https://developer.apple.com/documentation/swiftui/environment).
>
### 1. Point Free Style
> point free style 是 FP 的應用;我們使用各種事先定義好的 function 組合出我們要做的事情,這些 function 都不牽涉到數據的處理。
數據流與程式行為**分離**開來,使我們只關注在**做什麼**,而非**怎麼做**(如何處理數據)。 - [[Javascript] Point Free Style 如何幫助提高程式可讀性](https://medium.com/%E4%B8%80%E5%80%8B%E5%B0%8F%E5%B0%8F%E5%B7%A5%E7%A8%8B%E5%B8%AB%E7%9A%84%E9%9A%A8%E6%89%8B%E7%AD%86%E8%A8%98/javascript-point-free-style-%E5%A6%82%E4%BD%95%E5%B9%AB%E5%8A%A9%E6%8F%90%E9%AB%98%E7%A8%8B%E5%BC%8F%E5%8F%AF%E8%AE%80%E6%80%A7-fe865053a06d)
>
```swift=
let userList = [
User(id: 1, name: "Steven", age: 28, phone: "0912123123", location: location),
User(id: 2, name: "Denis", age: 30, location: location),
User(id: 3, name: "Wedy", age: 13, location: location)
]
userList.map { $0.name }
// v.s.
userList.map(\.name)
userList.filter { $0.isAdult }
// v.s.
userList.filter(\.isAdult)
userList.compactMap { $0.phone }
// v.s.
userList.compactMap(\.phone)
```
### 2. KVO
```swift=
class Person: NSObject {
@objc dynamic var age: Int
init(age: Int) {
self.age = age
}
func growth() {
age += 1
}
}
let me = Person(age: 27)
me.observe(\.age, options: [.initial, .old]) { person, changed in
print(changed.oldValue ?? "--", " > ", person.age)
}
me.growth()
/* Output
nil 27
Optional(27) 28
*/
```
### Combine
```swift=
let subject = PassthroughSubject<String, Never>()
subject.assign(to: \.text, on: label)
subject.send("HelloWorld")
label.text // "HelloWorld"
```
### KeyPath in Enum?
```swift=
enum Authentication {
case authenticated(accessToken: String)
case unauthenticated
}
\Authentication.unauthenticated
// ❌ Key path cannot refer to static member 'unauthenticated'
```
It’s hard to get associated value.
```swift=
let auth = Authentication.authenticated(accessToken: "aBcDE123")
// switch
switch auth {
case let .authenticated(accessToken):
print("authenticated: \(accessToken)")
case .unauthenticated:
break
}
// if case let
if case let Authentication.authenticated(accessToken) = auth {
print("authenticated: \(accessToken)")
}
```
## CasePath
> Case paths bring the power and ergonomics of key paths to enums!
They unlock the ability to do many things, like [key-value observing](https://developer.apple.com/documentation/swift/cocoa_design_patterns/using_key-value_observing_in_swift) and [reactive bindings](https://developer.apple.com/documentation/combine/receiving_and_handling_events_with_combine), [dynamic member lookup](https://github.com/apple/swift-evolution/blob/master/proposals/0252-keypath-dynamic-member-lookup.md), and scoping changes to the SwiftUI [environment](https://developer.apple.com/documentation/swiftui/environment).
>
```swift=
import CasePaths
```
**概念**上跟 KeyPath 相似,也是有一組 getter(extract) & setter(embed)
```swift=
struct CasePath<Root, Value> {
let extract: (Root) -> Value?
let embed: (Value) -> Root
}
```
**embed** (like getter...?)
```swift=
let _embedAuth = Authentication.authenticated(accessToken: "cafebeef")
// v.s.
let embedAuth = (/Authentication.authenticated).embed("cafebeef") // Returns a root by embedding a value.
```
**extract** (like setter...?)
```swift=
if case let Authentication.authenticated(accessToken) = _embedAuth {
print(accessToken)
}
// v.s.
(/Authentication.authenticated).extract(from: embedAuth) // Attempts to extract a value from a root.
(/Authentication.authenticated).extract(from: .unauthenticated) // nil
```
**example 1: compactMap**
```swift=
let authentications: [Authentication] = [
.unauthenticated,
.authenticated(accessToken: "abc"),
.authenticated(accessToken: "efg"),
.unauthenticated
]
authentications.compactMap { auth -> String? in
if case let .authenticated(accessToken) = auth {
return accessToken
}
return nil
}
// v.s.
authentications.compactMap(/Authentication.authenticated)
```
**example 2: compactMap**
```swift=
observable
.map {
switch $0 {
case .cancel:
return nil
case let .finish(user: user, auth: auth):
return (user: user, auth: auth)
}
}
.compactMap { $0 }
.bind(to: otherObserver)
.disposed(by: disposeBag)
// v.s.
observable
.compactMap(/FinishReason.finish)
.bind(to: otherObserver)
.disposed(by: disposeBag)
```
## Key points
- **key paths**: package up the functionality of getting and setting a value on a root structure.
- `user[keyPath: \User.phone]`
- `user[keyPath: \User.phone] = "0912312300"`
```swift=
struct _WritableKeyPath<Root, Value> {
let get: (Root) -> Value
let set: (inout Root, Value) -> Void
}
```
- **case paths**: package up the functionality of extracting and embedding a value on a root enumeration.
- `(/Authentication.authenticated).extract(from: embedAuth)`
- `(/Authentication.authenticated).embed("cafebeef")`
```swift=
struct CasePath<Root, Value> {
let extract: (Root) -> Value?
let embed: (Value) -> Root
}
```
## Supplement
1. 最起初的提案
- [Automatically derive properties for enum cases](https://github.com/stephencelis/swift-evolution/blob/enum-case-property-derivation/proposals/0000-automatically-derive-properties-for-enum-cases.md)
2. 本來是用 Reflection 實現,因為效能關係,以 Metadata 改寫。
- [[Pitch] Enum Quality of Life Improvements](https://forums.swift.org/t/pitch-enum-quality-of-life-improvements/52770/18)
2. 論壇有在討論是否要將 case path 納入 swift,但似乎受限於 compiler 設計,目前仍是討論中。
- [[Discussion] Inclusion of CasePaths into the language](https://forums.swift.org/t/discussion-inclusion-of-casepaths-into-the-language/53024)