Last active
March 12, 2018 19:36
-
-
Save tikitu/dfb1d72ff542a97021962c0e189616bd to your computer and use it in GitHub Desktop.
Problems with functional pipelines in Swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
// Copy/paste me into a playground, I'm ready to roll! | |
// We're going to start with UIKit styling functions in the style | |
// let label = UILabel() |> | |
// textColor(.blue) >>> backgroundColor(.red) | |
// then run into difficulties with generifying those functions over protocols, | |
// and end with an alternative using <> and a nonstandard |> | |
// (My first attempt was a bit messy, but thanks to @stephencelis the final version is pretty ok.) | |
// We'll use the pointfree.co operator definitions: | |
precedencegroup ForwardApplication { | |
associativity: left | |
} | |
infix operator |>: ForwardApplication | |
func |> <A, B>(a: A, f: (A) -> B) -> B { | |
return f(a) | |
} | |
precedencegroup ForwardComposition { | |
associativity: left | |
higherThan: ForwardApplication | |
} | |
infix operator >>>: ForwardComposition | |
func >>> <A, B, C>(f: @escaping (A) -> B, g: @escaping (B) -> C) -> ((A) -> C) { | |
return { a in | |
g(f(a)) | |
} | |
} | |
// Now we want to apply styling to UIKit components: | |
func background(_ color: UIColor) -> (UIView) -> UIView { | |
return { $0.backgroundColor = color; return $0 } | |
} | |
func textColor(_ color: UIColor) -> (UILabel) -> UILabel { | |
return { $0.textColor = color; return $0 } | |
} | |
// Now, though, we run into type difficulties when we try to compose: | |
let blackAndBlue = textColor(.blue) >>> background(.black) // this order is ok, but... | |
// let blackAndBlue = background(.black) >>> textColor(.blue) | |
// error: cannot convert value of type '(UILabel) -> UILabel' to expected argument type '(UIView) -> UILabel' | |
// Generics to the rescue! | |
func background2<V: UIView>(_ color: UIColor) -> (V) -> V { | |
return { $0.backgroundColor = color; return $0 } | |
} | |
let blackAndBlue2 = background2(.black) >>> textColor(.blue) | |
// Now we notice that both UILabel and UITextView have a textColor property, | |
// with one typed UIColor? and the other UIColor! -- perhaps we can smoothen | |
// out the annoying difference in optionality, and get a protocol we can reuse | |
// across both. | |
// (Let's ignore the question of whether this is a GOOD idea: | |
// it's just for the sake of a vivid example.) | |
// We'll also need another, different, styling function to show the problem, | |
// so let's do the same with a background color property. | |
protocol ColorSchemeComponentType: AnyObject { // We'll need the AnyObject restriction later, and it certainly does apply to UIKit components | |
var foreground: UIColor? { get set } | |
var background: UIColor? { get set } | |
} | |
extension UILabel: ColorSchemeComponentType { | |
var foreground: UIColor? { | |
get { return textColor } | |
set { textColor = newValue } | |
} | |
var background: UIColor? { | |
get { return backgroundColor } | |
set { backgroundColor = newValue } | |
} | |
} | |
extension UITextView: ColorSchemeComponentType { | |
var foreground: UIColor? { | |
get { return textColor } | |
set { textColor = newValue } | |
} | |
var background: UIColor? { | |
get { return backgroundColor } | |
set { backgroundColor = newValue } | |
} | |
} | |
// Now we need styling functions, and we know we should make them generic: | |
func foreground<A: ColorSchemeComponentType>(_ color: UIColor) -> (A) -> A { | |
return { $0.foreground = color; return $0 } | |
} | |
func background3<A: ColorSchemeComponentType>(_ color: UIColor) -> (A) -> A { | |
return { $0.background = color; return $0 } | |
} | |
// Finally, we're ready to show the problem: | |
// error: binary operator '>>>' cannot be applied to two '(_) -> _' operands | |
// let headerStyle = foreground(.blue) >>> background3(.red) | |
// Generic type parameters need to be fully specified when storing into a variable. | |
// So we can't define a single header style that can be used for both | |
// UILabel and UITextView (which was the point of extracting the protocol). | |
let headerStyle: (UILabel) -> UILabel = foreground(.blue) >>> background3(.red) | |
// This greatly restricts our ability to work with these styling functions! | |
// Here's an alternative that works, at the cost of a slightly non-standard |> definition: | |
func |> <A: AnyObject>(a: A, f: (A) -> Void) -> A { | |
f(a) | |
return a | |
} | |
precedencegroup SingleTypeComposition { | |
associativity: left | |
higherThan: ForwardApplication | |
} | |
infix operator <>: SingleTypeComposition | |
func <> <A>( | |
f: @escaping (A) -> Void, | |
g: @escaping (A) -> Void) | |
-> (A) -> Void { | |
return { a in | |
f(a) | |
g(a) | |
} | |
} | |
func foreground2(_ color: UIColor) -> (ColorSchemeComponentType) -> Void { | |
return { $0.foreground = color } | |
} | |
func background4(_ color: UIColor) -> (ColorSchemeComponentType) -> Void { | |
return { $0.background = color } | |
} | |
let headerStyle2 = foreground2(.blue) <> background4(.red) | |
// Now this styling function (because it's not generic at all) can be reused | |
// across all ColorSchemeComponentType-conforming types: | |
let label: UILabel = UILabel() |> headerStyle2 | |
let textView: UITextView = UITextView() |> headerStyle2 | |
// Somewhat to my surprise, these functions even compose with adhoc closures that | |
// don't reference ColorSchemeComponentType at all: | |
UILabel() |> headerStyle2 <> { $0.text = "some text" } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment