Skip to content

Instantly share code, notes, and snippets.

@tikitu
Last active March 12, 2018 19:36
Show Gist options
  • Save tikitu/dfb1d72ff542a97021962c0e189616bd to your computer and use it in GitHub Desktop.
Save tikitu/dfb1d72ff542a97021962c0e189616bd to your computer and use it in GitHub Desktop.
Problems with functional pipelines in Swift
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