diff --git a/0088-the-case-for-case-paths-pt2/CasePath.playground/Contents.swift b/0088-the-case-for-case-paths-pt2/CasePath.playground/Contents.swift new file mode 100644 index 00000000..243b19fe --- /dev/null +++ b/0088-the-case-for-case-paths-pt2/CasePath.playground/Contents.swift @@ -0,0 +1,288 @@ +import Foundation + +struct User { + var id: Int + var isAdmin: Bool + var location: Location + var name: String +} +struct Location { + var city: String + var country: String +} + +\User.id as WritableKeyPath +\User.isAdmin as KeyPath +\User.name + +var user = User(id: 42, isAdmin: true, location: Location(city: "Brooklyn", country: "USA"), name: "Blob") + +user[keyPath: \.id] + +user[keyPath: \.id] = 57 + +user.id = 57 +user.name = "Blob Jr." + +class Label: NSObject { + @objc dynamic var font = "Helvetica" + @objc dynamic var fontSize = 12 + @objc dynamic var text = "" +} + +class Model: NSObject { + @objc dynamic var userName = "" +} + +let model = Model() +let label = Label() + +bind(model: model, \.userName, to: label, \.text) + +//bind(model: model, get: { $0.userName }, to: label, get: { $0.text }, set: { $0.text = $1 }) + +label.text +model.userName = "blob" +label.text +model.userName = "XxFP_FANxX93" +label.text + +import Combine + +let subject = PassthroughSubject() +subject.assign(to: \.text, on: label) + +subject.send("MaTh_FaN96") +label.text + + +typealias Reducer = (inout Value, Action) -> Void + +[1, 2, 3] + .reduce(into: 0, { $0 += $1 }) +[1, 2, 3] + .reduce(into: 0, +=) + + +func pullback( + _ reducer: @escaping Reducer, + value: WritableKeyPath +) -> Reducer { + + return { globalValue, action in +// var localValue = globalValue[keyPath: value] +// reducer(&localValue, action) +// globalValue[keyPath: value] = localValue + reducer(&globalValue[keyPath: value], action) + } +} + +func pullback( + _ reducer: @escaping Reducer, + getLocalValue: @escaping (GlobalValue) -> LocalValue, + setLocalValue: @escaping (inout GlobalValue, LocalValue) -> Void +) -> Reducer { + + return { globalValue, action in + var localValue = getLocalValue(globalValue) + reducer(&localValue, action) + setLocalValue(&globalValue, localValue) + } +} + +let counterReducer: Reducer = { count, _ in count += 1 } + +pullback(counterReducer, value: \User.id) + +[1, 2, 3] + .map(String.init) + +pullback( + counterReducer, + getLocalValue: { (user: User) in user.id }, + setLocalValue: { $0.id = $1 } +) + + +struct _WritableKeyPath { + let get: (Root) -> Value + let set: (inout Root, Value) -> Void +} + + +// [user valueForKeyPath:@"location.city"] + +struct CasePath { + let extract: (Root) -> Value? + let embed: (Value) -> Root +} + +extension Result { + static var successCasePath: CasePath { + CasePath( + extract: { result -> Success? in + if case let .success(value) = result { + return value + } + return nil + }, + embed: Result.success + ) + } + + static var failureCasePath: CasePath { + CasePath( + extract: { result -> Failure? in + if case let .failure(value) = result { + return value + } + return nil + }, + embed: Result.failure + ) + } +} + +Result.successCasePath +Result.failureCasePath + +\User.location +\Location.city + +(\User.location).appending(path: \Location.city) +\User.location.city + +extension CasePath/**/ { + func appending( + path: CasePath + ) -> CasePath { + CasePath( + extract: { root in + self.extract(root).flatMap(path.extract) + }, + embed: { appendedValue in + self.embed(path.embed(appendedValue)) + }) + } +} + +enum Authentication { + case authenticated(AccessToken) + case unauthenticated +} + +struct AccessToken { + var token: String +} + +let authenticatedCasePath = CasePath( + extract: { + if case let .authenticated(accessToken) = $0 { return accessToken } + return nil +}, + embed: Authentication.authenticated +) + +Result.successCasePath + .appending(path: authenticatedCasePath) +// CasePath, AccessToken> + +\User.location.city + +precedencegroup Composition { + associativity: right +} +infix operator ..: Composition + +func .. ( + lhs: CasePath, + rhs: CasePath + ) -> CasePath { + lhs.appending(path: rhs) +} + +Result.successCasePath .. authenticatedCasePath + +\User.self +\Location.self +\String.self +\Int.self + +extension CasePath where Root == Value { + static var `self`: CasePath { + return CasePath( + extract: { .some($0) }, + embed: { $0 } + ) + } +} + +CasePath.`self` + + +prefix operator ^ +prefix func ^ ( + _ kp: KeyPath +) -> (Root) -> Value { + return { root in root[keyPath: kp] } +} + + +let users = [ + User( + id: 1, + isAdmin: true, + location: Location(city: "Brooklyn", country: "USA"), + name: "Blob" + ), + User( + id: 2, + isAdmin: false, + location: Location(city: "Los Angeles", country: "USA"), + name: "Blob Jr." + ), + User( + id: 3, + isAdmin: true, + location: Location(city: "Copenhagen", country: "DK"), + name: "Blob Sr." + ), +] + +users + .map(^\.name) +users + .map(^\.id) +users + .filter(^\.isAdmin) + + +//users.map(\.name) +//users.map(\.location.city) +//users.filter(\.isAdmin) + +prefix func ^ ( + path: CasePath + ) -> (Root) -> Value? { + return path.extract +} + +^authenticatedCasePath + + +let authentications: [Authentication] = [ + .authenticated(AccessToken(token: "deadbeef")), + .unauthenticated, + .authenticated(AccessToken(token: "cafed00d")) +] + +authentications + .compactMap(^authenticatedCasePath) + +authentications + .compactMap { (authentication) -> AccessToken? in + if case let .authenticated(accessToken) = authentication { + return accessToken + } + return nil +} diff --git a/0088-the-case-for-case-paths-pt2/CasePath.playground/Sources/Bind.swift b/0088-the-case-for-case-paths-pt2/CasePath.playground/Sources/Bind.swift new file mode 100644 index 00000000..66bd6fda --- /dev/null +++ b/0088-the-case-for-case-paths-pt2/CasePath.playground/Sources/Bind.swift @@ -0,0 +1,15 @@ +import Foundation + +public func bind( + model: Model, + _ modelKeyPath: KeyPath, + to target: Target, + _ targetKeyPath: ReferenceWritableKeyPath +) { + var observation: NSKeyValueObservation! + observation = model.observe(modelKeyPath, options: [.initial, .new]) { _, change in + guard let newValue = change.newValue else { return } + target[keyPath: targetKeyPath] = newValue + _ = observation + } +} diff --git a/0088-the-case-for-case-paths-pt2/CasePath.playground/contents.xcplayground b/0088-the-case-for-case-paths-pt2/CasePath.playground/contents.xcplayground new file mode 100644 index 00000000..63b6dd8d --- /dev/null +++ b/0088-the-case-for-case-paths-pt2/CasePath.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/0088-the-case-for-case-paths-pt2/README.md b/0088-the-case-for-case-paths-pt2/README.md new file mode 100644 index 00000000..0fb667e4 --- /dev/null +++ b/0088-the-case-for-case-paths-pt2/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [The Case for Case Paths: Properties](https://www.pointfree.co/episodes/ep88-the-case-for-case-paths-properties) +> +> We've now seen that it's possible to define "case paths": the enum equivalent of key paths. So what are their features? Let's explore a few properties of key paths to see if there are corresponding concepts on case paths. diff --git a/README.md b/README.md index 7f859ea7..138cd8b9 100644 --- a/README.md +++ b/README.md @@ -90,3 +90,4 @@ This repository is the home of code written on episodes of 1. [Effectful State Management: The Point](0085-testable-state-management-the-point) 1. [SwiftUI Snapshot Testing](0086-swiftui-snapshot-testing) 1. [The Case for Case Paths: Introduction](0087-the-case-for-case-paths-pt1) +1. [The Case for Case Paths: Properties](0088-the-case-for-case-paths-pt2)