From 775eeee475a3bd1d57aa279e83c03f52084b0c51 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 14 May 2018 10:06:14 -0400 Subject: [PATCH] Add Setters --- 0013-the-many-faces-of-map/README.md | 2 +- 0014-contravariance/README.md | 2 +- 0015-setters-pt-3/README.md | 8 + .../Contents.swift | 213 ++++++++++++++++++ .../Contents.swift | 22 ++ .../Setters-3.playground/Sources/Pipe.swift | 6 + .../Setters-3.playground/Sources/Util.swift | 132 +++++++++++ .../contents.xcplayground | 7 + 8 files changed, 390 insertions(+), 2 deletions(-) create mode 100644 0015-setters-pt-3/README.md create mode 100644 0015-setters-pt-3/Setters-3.playground/Pages/01-episode.xcplaygroundpage/Contents.swift create mode 100644 0015-setters-pt-3/Setters-3.playground/Pages/02-exercises.xcplaygroundpage/Contents.swift create mode 100644 0015-setters-pt-3/Setters-3.playground/Sources/Pipe.swift create mode 100644 0015-setters-pt-3/Setters-3.playground/Sources/Util.swift create mode 100644 0015-setters-pt-3/Setters-3.playground/contents.xcplayground diff --git a/0013-the-many-faces-of-map/README.md b/0013-the-many-faces-of-map/README.md index bf84ba88..99c9783d 100644 --- a/0013-the-many-faces-of-map/README.md +++ b/0013-the-many-faces-of-map/README.md @@ -1,4 +1,4 @@ -### [Point-Free](https://www.pointfree.co) The Many Faces of Map #13 +### [Point-Free](https://www.pointfree.co) Episode #13 ### The Many Faces of Map diff --git a/0014-contravariance/README.md b/0014-contravariance/README.md index 0f0272cd..a77015c9 100644 --- a/0014-contravariance/README.md +++ b/0014-contravariance/README.md @@ -1,4 +1,4 @@ -### [Point-Free](https://www.pointfree.co) Contravariance #14 +### [Point-Free](https://www.pointfree.co) Episode #14 ### Contravariance diff --git a/0015-setters-pt-3/README.md b/0015-setters-pt-3/README.md new file mode 100644 index 00000000..ca8aa631 --- /dev/null +++ b/0015-setters-pt-3/README.md @@ -0,0 +1,8 @@ +### [Point-Free](https://www.pointfree.co) Episode #15 + +### Setters: Ergonomics & Performance + +> Functional setters can be very powerful, but the way we have defined them so far is not super ergonomic or performant. We will provide a friendlier API to use setters and take advantage of Swift's value mutation semantics to make setters a viable tool to bring into your code base _today_. + +This directory contains code from Point-Free Episode #15: +[Setters: Ergonomics & Performance](https://www.pointfree.co/episodes/ep15-setters-ergonomics-performance) diff --git a/0015-setters-pt-3/Setters-3.playground/Pages/01-episode.xcplaygroundpage/Contents.swift b/0015-setters-pt-3/Setters-3.playground/Pages/01-episode.xcplaygroundpage/Contents.swift new file mode 100644 index 00000000..f045f500 --- /dev/null +++ b/0015-setters-pt-3/Setters-3.playground/Pages/01-episode.xcplaygroundpage/Contents.swift @@ -0,0 +1,213 @@ + +struct Food { + var name: String +} + +struct Location { + var name: String +} + +struct User { + var favoriteFoods: [Food] + var location: Location + var name: String +} + +let user = User( + favoriteFoods: [Food(name: "Tacos"), Food(name: "Nachos")], + location: Location(name: "Brooklyn"), + name: "Blob" +) + +func prop(_ kp: WritableKeyPath) + -> (@escaping (Value) -> Value) + -> (Root) -> Root { + + return { update in + { root in + var copy = root + copy[keyPath: kp] = update(copy[keyPath: kp]) + return copy + } + } +} + +//user +// |> prop(\.name)({ $0.uppercased() }) +// <> prop(\.location.name)({ _ in "Los Angeles" }) + +func prop( + _ kp: WritableKeyPath, + _ f: @escaping (Value) -> Value + ) + -> (Root) -> Root { + + return prop(kp)(f) +} +func prop( + _ kp: WritableKeyPath, + _ value: Value + ) + -> (Root) -> Root { + + return prop(kp, { _ in value }) +} + +let addCourtesyTitle = { $0 + ", Esq." } +let healthierOption = { $0 + " & Salad" } + +//user +// |> prop(\.name, addCourtesyTitle) +// <> prop(\.name) { $0.uppercased() } +//// <> prop(\.location.name) { _ in "Los Angeles" } +// <> prop(\.location.name, "Los Angeles") +//// <> (prop(\.favoriteFoods) <<< map <<< prop(\.name)) { $0 + " & Salad" } +// <> (prop(\.favoriteFoods) <<< map <<< prop(\.name))(healthierOption) + +typealias Setter = (@escaping (A) -> B) -> (S) -> T + +// ((A) -> B) -> ([A]) -> [B] +// ((A) -> B) -> (A?) -> B? +// ((A) -> B) -> (A, C) -> (B, C) + +func over( + _ setter: Setter, + _ f: @escaping (A) -> B + ) -> (S) -> T { + + return setter(f) +} + +func set( + _ setter: Setter, + _ value: B + ) -> (S) -> T { + + return over(setter, { _ in value }) +} + +//user +// |> over(prop(\.name), addCourtesyTitle) +// <> over(prop(\.name)) { $0.uppercased() } +// // <> prop(\.location.name) { _ in "Los Angeles" } +// <> set(prop(\.location.name), "Los Angeles") +// // <> (prop(\.favoriteFoods) <<< map <<< prop(\.name)) { $0 + " & Salad" } +// <> over(prop(\.favoriteFoods) <<< map <<< prop(\.name), healthierOption) + +//^\User.name + +prefix func ^ (_ kp: WritableKeyPath) + -> (@escaping (Value) -> Value) + -> (Root) -> Root { + + return prop(kp) +} + +//user +// |> over(^\.name, addCourtesyTitle) +// <> over(^\.name) { $0.uppercased() } +// <> set(^\.location.name, "Los Angeles") +// <> over(^\.favoriteFoods <<< map <<< ^\.name, healthierOption) + +//("Hello, world", 42) +// |> set(first, [1, 2, 3]) +// |> over(second, String.init) + + + +//let guaranteeHeaders = over(^\URLRequest.allHTTPHeaderFields) { $0 ?? [:] } +// +//let setHeader = { name, value in +// guaranteeHeaders +// <> set(^\.allHTTPHeaderFields <<< map <<< ^\.[name], value) +//} +// +//let postJson = +// set(^\.httpMethod, "POST") +// <> setHeader("Content-Type", "application/json; charset=utf-8") +// +//let gitHubAccept = +// setHeader("Accept", "application/vnd.github.v3+json") +// +//let attachAuthorization = { token in +// setHeader("Authorization", "Token " + token) +//} +// +//URLRequest(url: URL(string: "https://www.pointfree.co/hello")!) +// |> attachAuthorization("deadbeef") +// <> gitHubAccept +// <> postJson + + +typealias MutableSetter = (@escaping (inout A) -> Void) -> (inout S) -> Void + +func mver( + _ setter: MutableSetter, + _ f: @escaping (inout A) -> Void + ) -> (inout S) -> Void { + + return setter(f) +} + +func mut( + _ setter: MutableSetter, + _ value: A + ) -> (inout S) -> Void { + + return mver(setter, { $0 = value }) +} + + +prefix func ^ ( + _ kp: WritableKeyPath + ) + -> (@escaping (inout Value) -> Void) + -> (inout Root) -> Void { + + return { update in + return { root in + update(&root[keyPath: kp]) + } + } +} + + +func mutEach(_ f: @escaping (inout A) -> Void) -> (inout [A]) -> Void { + return { + for i in $0.indices { + f(&$0[i]) + } + } +} + + + +let newUser = user + |> mver(^\.name) { $0 = $0.uppercased() } + <> mut(^\.location.name, "Los Angeles") + <> mver(^\.favoriteFoods <<< mutEach <<< ^\.name) { $0 += " & Salad" } + + +let guaranteeHeaders = mver(^\URLRequest.allHTTPHeaderFields) { $0 = $0 ?? [:] } + +let setHeader = { name, value in + guaranteeHeaders + <> { $0.allHTTPHeaderFields?[name] = value } +} + +let postJson = + mut(^\.httpMethod, "POST") + <> setHeader("Content-Type", "application/json; charset=utf-8") + +let gitHubAccept = + setHeader("Accept", "application/vnd.github.v3+json") + +let attachAuthorization = { token in + setHeader("Authorization", "Token " + token) +} + +let request = URLRequest(url: URL(string: "https://www.pointfree.co/hello")!) + |> attachAuthorization("deadbeef") + <> gitHubAccept + <> postJson +//: [See the next page](@next) for exercises! diff --git a/0015-setters-pt-3/Setters-3.playground/Pages/02-exercises.xcplaygroundpage/Contents.swift b/0015-setters-pt-3/Setters-3.playground/Pages/02-exercises.xcplaygroundpage/Contents.swift new file mode 100644 index 00000000..d9ca1241 --- /dev/null +++ b/0015-setters-pt-3/Setters-3.playground/Pages/02-exercises.xcplaygroundpage/Contents.swift @@ -0,0 +1,22 @@ +/*: + # Setters: Ergonomics & Performance Exercises + + 1.) We previously saw that functions `(inout A) -> Void` and functions `(A) -> Void where A: AnyObject` can be composed the same way. Write `mver`, `mut`, and `^` in terms of `AnyObject`. Note that there is a specific subclass of `WritableKeyPath` for reference semantics. + */ +// TODO +/*: + 2.) Our [episode on UIKit styling](/episodes/ep3-uikit-styling-with-functions) was nothing more than setters in disguise! Explore building some of the styling functions we covered using both immutable and mutable setters, specifically how setters compose over sub-typing in Swift, and how setters compose between roots that are reference types, and values that are value types. + */ +// TODO +/*: + 3.) We've explored `<>`/`concat` as single-type composition, but this doesn't mean we're limited to a single generic parameter! Write a version of `<>`/`concat` that allows for composition of value transformations of the same input and output type. This should allow for `prop(\\UIEdgeInsets.top) <> prop(\\.bottom)` as a way of assigning both `top` and `bottom` the same value at once. + */ +// TODO +/*: + 4.) Define an operator-free version of setters using `with` and `concat` from our episode on [composition without operators](/episodes/ep11-composition-without-operators). Define an `update` function that combines the semantics of `with` and the variadic convenience of `concat` for ergonomics. + */ +// TODO +/*: + 5.) In the Haskell Lens library, `over` and `set` are defined as infix operators `%~` and `.~`. Define these operators and explore what their precedence should be, updating some of our examples to use them. Do these operators tick the boxes? + */ +// TODO diff --git a/0015-setters-pt-3/Setters-3.playground/Sources/Pipe.swift b/0015-setters-pt-3/Setters-3.playground/Sources/Pipe.swift new file mode 100644 index 00000000..1ea50bab --- /dev/null +++ b/0015-setters-pt-3/Setters-3.playground/Sources/Pipe.swift @@ -0,0 +1,6 @@ + +public func |> (_ a: A, _ f: (inout A) -> Void) -> A { + var a = a + f(&a) + return a +} diff --git a/0015-setters-pt-3/Setters-3.playground/Sources/Util.swift b/0015-setters-pt-3/Setters-3.playground/Sources/Util.swift new file mode 100644 index 00000000..af891558 --- /dev/null +++ b/0015-setters-pt-3/Setters-3.playground/Sources/Util.swift @@ -0,0 +1,132 @@ +@_exported import Foundation + +precedencegroup ForwardApplication { + associativity: left + higherThan: AssignmentPrecedence +} + +infix operator |>: ForwardApplication + +precedencegroup ForwardComposition { + associativity: left + higherThan: ForwardApplication +} + +infix operator >>>: ForwardComposition + +public func |> (x: A, f: (A) -> B) -> B { + return f(x) +} + +public func >>> (f: @escaping (A) -> B, g: @escaping (B) -> C) -> (A) -> C { + return { g(f($0)) } +} + +public func zurry(_ f: () -> A) -> A { + return f() +} + +public func flip(_ f: @escaping (A) -> (B) -> C) -> (B) -> (A) -> C { + + return { b in { a in f(a)(b) } } +} + +public func flip(_ f: @escaping (A) -> () -> C) -> () -> (A) -> C { + + return { { a in f(a)() } } +} + + +precedencegroup SingleTypeComposition { + associativity: left + higherThan: ForwardApplication +} + +infix operator <>: SingleTypeComposition + +public func <> (f: @escaping (A) -> A, g: @escaping (A) -> A) -> (A) -> A { + return f >>> g +} + +public func <> (f: @escaping (inout A) -> Void, g: @escaping (inout A) -> Void) -> (inout A) -> Void { + return { a in + f(&a) + g(&a) + } +} + +public func incr(_ x: Int) -> Int { + return x + 1 +} + +public func square(_ x: Int) -> Int { + return x * x +} + +precedencegroup BackwardsComposition { + associativity: left +} +infix operator <<<: BackwardsComposition +public func <<< (_ f: @escaping (B) -> C, _ g: @escaping (A) -> B) -> (A) -> C { + return { f(g($0)) } +} + +public func first(_ f: @escaping (A) -> C) -> ((A, B)) -> (C, B) { + return { pair in + return (f(pair.0), pair.1) + } +} + +public func second(_ f: @escaping (B) -> C) -> ((A, B)) -> (A, C) { + return { pair in + return (pair.0, f(pair.1)) + } +} + +public func map(_ f: @escaping (A) -> B) -> ([A]) -> [B] { + return { $0.map(f) } +} + +public func map(_ f: @escaping (A) -> B) -> (A?) -> B? { + return { $0.map(f) } +} + +public func with(_ a: A, _ f: (A) -> B) -> B { + return f(a) +} + +public func with(_ a: inout A, _ f: (inout A) -> Void) -> Void { + f(&a) +} + +public func concat( + _ f1: @escaping (A) -> A, + _ f2: @escaping (A) -> A, + _ fs: ((A) -> A)... + ) + -> (A) -> A { + + return { a in + fs.reduce(f2(f1(a))) { a, f in f(a) } + } +} + +public func concat( + _ f1: @escaping (inout A) -> Void, + _ f2: @escaping (inout A) -> Void, + _ fs: ((inout A) -> Void)... + ) + -> (inout A) -> Void { + + return { a in + f1(&a) + f2(&a) + fs.forEach { f in f(&a) } + } +} + +prefix operator ^ + +public prefix func ^ (kp: KeyPath) -> (Root) -> Value { + return { $0[keyPath: kp] } +} diff --git a/0015-setters-pt-3/Setters-3.playground/contents.xcplayground b/0015-setters-pt-3/Setters-3.playground/contents.xcplayground new file mode 100644 index 00000000..8bd8b172 --- /dev/null +++ b/0015-setters-pt-3/Setters-3.playground/contents.xcplayground @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file