From efabded1c7772dfb2890ced99135610ffc6b0df2 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Sun, 2 Feb 2020 23:39:33 -0500 Subject: [PATCH] 89 --- .../CasePath.playground/Contents.swift | 444 ++++++++++++++++++ .../CasePath.playground/Sources/Bind.swift | 15 + .../CasePath.playground/contents.xcplayground | 4 + 0089-the-case-for-case-paths-pt3/README.md | 5 + README.md | 1 + 5 files changed, 469 insertions(+) create mode 100644 0089-the-case-for-case-paths-pt3/CasePath.playground/Contents.swift create mode 100644 0089-the-case-for-case-paths-pt3/CasePath.playground/Sources/Bind.swift create mode 100644 0089-the-case-for-case-paths-pt3/CasePath.playground/contents.xcplayground create mode 100644 0089-the-case-for-case-paths-pt3/README.md diff --git a/0089-the-case-for-case-paths-pt3/CasePath.playground/Contents.swift b/0089-the-case-for-case-paths-pt3/CasePath.playground/Contents.swift new file mode 100644 index 00000000..56d63427 --- /dev/null +++ b/0089-the-case-for-case-paths-pt3/CasePath.playground/Contents.swift @@ -0,0 +1,444 @@ +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 +} + +func allProperties(_ value: Any) -> [String] { + let mirror = Mirror(reflecting: value) + return mirror.children.compactMap { child in child.label } +} + +allProperties(user) + + +let auth = Authentication.authenticated(AccessToken(token: "deadbeef")) + +let mirror = Mirror(reflecting: auth) +dump(mirror.children.first!) + +mirror.children.first!.value as? AccessToken + +func extractHelp( + case: (Value) -> Root, + from root: Root +) -> Value? { + let mirror = Mirror(reflecting: root) + guard let child = mirror.children.first else { return nil } + guard let value = child.value as? Value else { return nil } + + let newRoot = `case`(value) + let newMirror = Mirror(reflecting: newRoot) + guard let newChild = newMirror.children.first else { return nil } + guard newChild.label == child.label else { return nil } + + return value +} + +extractHelp(case: Authentication.authenticated, from: auth) +extractHelp(case: Authentication.authenticated, from: .unauthenticated) + +extractHelp(case: Result.success, from: .success(42)) + +struct MyError: Error {} + +extractHelp(case: Result.failure, from: .failure(MyError())) + + +enum Example { + case foo(Int) + case bar(Int) +} + +Example.foo +Example.bar + +extractHelp(case: Example.foo, from: .foo(2)) +extractHelp(case: Example.bar, from: .foo(2)) + +mirror.children.first!.label + +extension CasePath { + init(_ embed: @escaping (Value) -> Root) { + self.embed = embed + self.extract = { root in extractHelp(case: embed, from: root) } + } +} + +CasePath(Example.foo) +CasePath(Example.bar) +CasePath(Result.success) + + +enum ExampleWithArgumentLabels { + case foo(value: Int) +} + +extractHelp(case: ExampleWithArgumentLabels.foo, from: .foo(value: 42)) + +//extractHelp(case: Authentication.unauthenticated, from: .unauthenticated) + +let locationCountryCasePath = CasePath( +{ country in Location(city: "Brooklyn", country: country) } +) +locationCountryCasePath.extract(user.location) + + +CasePath(Result.success) +CasePath(Result.success) +CasePath(Result.failure) +CasePath(Result.failure) + + +CasePath(Optional.some) +CasePath(Optional.some) +CasePath(Optional<[Int]>.some) + + +CasePath(DispatchTimeInterval.seconds) +CasePath(DispatchTimeInterval.milliseconds) +CasePath(DispatchTimeInterval.microseconds) +CasePath(DispatchTimeInterval.nanoseconds) + + +CasePath(Subscribers.Completion.failure) + +prefix operator / + +prefix func / ( + case: @escaping (Value) -> Root +) -> CasePath { + CasePath(`case`) +} + +\User.id +/DispatchTimeInterval.seconds + +CasePath(DispatchTimeInterval.seconds) + +/Result.success .. /DispatchTimeInterval.seconds + +enum LoadState { + case loading + case offline + case loaded(Result) +} + +/LoadState.loaded .. /Result.success + +let states1: [LoadState] = [ + .loaded(.success(2)), + .loaded(.failure(NSError(domain: "", code: 1, userInfo: [:]))), + .loaded(.success(3)), + .loading, + .loaded(.success(4)), + .offline, +] + +states1 + .compactMap(^(/LoadState.loaded .. /Result.success)) + + +let states2: [LoadState] = [ + .loading, + .loaded(.success(.authenticated(AccessToken(token: "deadbeef")))), + .loaded(.failure(NSError(domain: "", code: 1, userInfo: [:]))), + .loaded(.success(.authenticated(AccessToken(token: "cafed00d")))), + .loaded(.success(.unauthenticated)), + .offline, +] + +/LoadState.loaded .. /Result.success .. /Authentication.authenticated +/LoadState.loaded .. /Result.success .. /Authentication.authenticated + +states2 + .compactMap(^(/LoadState.loaded .. /Result.success .. /Authentication.authenticated)) + +states2 + .compactMap { state -> AccessToken? in + if case let .loaded(.success(.authenticated(accessToken))) = state { return accessToken } + return nil +} diff --git a/0089-the-case-for-case-paths-pt3/CasePath.playground/Sources/Bind.swift b/0089-the-case-for-case-paths-pt3/CasePath.playground/Sources/Bind.swift new file mode 100644 index 00000000..66bd6fda --- /dev/null +++ b/0089-the-case-for-case-paths-pt3/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/0089-the-case-for-case-paths-pt3/CasePath.playground/contents.xcplayground b/0089-the-case-for-case-paths-pt3/CasePath.playground/contents.xcplayground new file mode 100644 index 00000000..63b6dd8d --- /dev/null +++ b/0089-the-case-for-case-paths-pt3/CasePath.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/0089-the-case-for-case-paths-pt3/README.md b/0089-the-case-for-case-paths-pt3/README.md new file mode 100644 index 00000000..85134415 --- /dev/null +++ b/0089-the-case-for-case-paths-pt3/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [Case Paths for Free](https://www.pointfree.co/episodes/ep89-case-paths-for-free) +> +> Although case paths are powerful and a natural extension of key paths, they are difficult to work with right now. They require either hand-written boilerplate, or code generation. However, there's another way to generate case paths for free, and it will make them just as ergonomic to use as key paths. diff --git a/README.md b/README.md index 138cd8b9..8bc01113 100644 --- a/README.md +++ b/README.md @@ -91,3 +91,4 @@ This repository is the home of code written on episodes of 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) +1. [Case Paths for Free](0089-the-case-for-case-paths-pt3)