# 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)