Last active
March 29, 2024 19:44
-
-
Save kristopherjohnson/ed97acf0bbe0013df8af to your computer and use it in GitHub Desktop.
Swift: define F#-style pipe-forward (|>) operator that evaluates from left to right.
This file contains hidden or 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
// F#'s "pipe-forward" |> operator | |
// | |
// Also "Optional-chaining" operators |>! and |>& | |
// | |
// And adapters for standard library map/filter/sorted | |
infix operator |> { precedence 50 associativity left } | |
infix operator |>! { precedence 50 associativity left } | |
infix operator |>& { precedence 50 associativity left } | |
infix operator |>* { precedence 50 associativity left } | |
// Pipe forward: transform "x |> f" to "f(x)" and "x |> f |> g |> h" to "h(g(f(x)))" | |
public func |> <T,U>(lhs: T, rhs: T -> U) -> U { | |
return rhs(lhs) | |
} | |
// Unwrap the optional value on the left-hand side and apply the right-hand function | |
public func |>! <T,U>(lhs: T?, rhs: T -> U) -> U { | |
return rhs(lhs!) | |
} | |
// If Optional value on left is nil, return nil; otherwise unwrap it and | |
// apply the right-hand function to it. | |
// | |
// (It would be nice if we could name this |>?, but the ? character | |
// is not allowed in custom operators.) | |
public func |>& <T,U>(lhs: T?, rhs: T -> U) -> U? { | |
return lhs.map(rhs) | |
} | |
// Transform "x |>* (f, predicate)" to "f(x, predicate)" | |
public func |>* <A, B, C, T>(lhs: A, rhs: ((A, (B) -> C) -> T, (B) -> C)) -> T { | |
return (rhs.0)(lhs, rhs.1) | |
} | |
// Examples | |
// List of random numbers | |
let numbers = [67, 83, 4, 99, 22, 18, 21, 24, 23, 2, 86] | |
// Get the even numbers, sort them in descending order, | |
// and render the result as a comma-separated string | |
let result = ", ".join(numbers.filter { $0 % 2 == 0 } | |
.sorted { $1 < $0 } | |
.map { $0.description }) | |
result | |
// Was easy for Array, let's try it as a Sequence with free functions | |
let seq = SequenceOf(numbers) | |
// All on one line | |
let seqResult = | |
", ".join(map(sorted(filter(seq){$0 % 2 == 0}){$1 < $0}){$0.description}) | |
// Ugly/unreadable | |
let seqResultMultiLine = | |
", ".join( | |
map( | |
sorted( | |
filter(seq) { $0 % 2 == 0 } | |
) { $1 < $0 } | |
) { $0.description }) | |
// Readable but verbose/noisy | |
let filteredNumbers = filter(seq) { $0 % 2 == 0 } | |
let sortedNumbers = sorted(filteredNumbers) { $1 < $0 } | |
let numbersAsStrings = map(sortedNumbers) { (n: Int) -> String in n.description } | |
let commaSeparated = ", ".join(numbersAsStrings) | |
// Using lazy() | |
let lazyResult = | |
", ".join(lazy(seq).filter({ $0 % 2 == 0 }).array | |
.sorted { $1 < $0 } | |
.map { $0.description }) | |
// Can do this with pipe-forward, but need adapters for filter/sorted/map/join. | |
// | |
// Each adapter must be curried, and the sequence to be operated on must be the final argument. | |
// Curried adapter function for Swift Standard Library's filter() function | |
public func filteredWithPredicate<S : SequenceType> | |
(includeElement: (S.Generator.Element) -> Bool) | |
(source: S) | |
-> [S.Generator.Element] | |
{ | |
return filter(source, includeElement) | |
} | |
// Curried adapter function for Swift Standard Library's sorted() function | |
public func sortedByPredicate<S : SequenceType> | |
(predicate: (S.Generator.Element, S.Generator.Element) -> Bool) | |
(source: S) | |
-> [S.Generator.Element] | |
{ | |
return sorted(source, predicate) | |
} | |
// Curried adapter function for Swift Standard Library's map() function | |
public func mappedWithTransform<S: SequenceType, T> | |
(transform: (S.Generator.Element) -> T) | |
(source: S) | |
-> [T] | |
{ | |
return map(source, transform) | |
} | |
let pipeResult = | |
seq |> filteredWithPredicate { $0 % 2 == 0 } | |
|> sortedByPredicate { $1 < $0 } | |
|> mappedWithTransform { $0.description } | |
|> String.join(", ") | |
// Instead of using the adapters, use equivalent closures | |
let closuresResult = | |
seq |> { s in filter(s) { $0 % 2 == 0 } } | |
|> { s in sorted(s) { $1 < $0 } } | |
|> { s in map(s) { $0.description } } | |
|> String.join(", ") | |
// Tuples | |
import Foundation | |
func diagonalLength(width: Double, height: Double) -> Double { | |
return sqrt(width * width + height * height) | |
} | |
let length = (3, 4) |> diagonalLength | |
func multiplyAndDivide(multiplier1: Double, multiplier2: Double, divisor: Double) -> Double { | |
return multiplier1 * multiplier2 / divisor | |
} | |
let value = (10, 20, 50) |> multiplyAndDivide | |
let evenNumbers = (seq, { $0 % 2 == 0 }) |> filter | |
// Optional chaining | |
let elements = [2, 4, 6, 8, 10] | |
func reportIndexOfValue(value: Int)(index: Int) -> String { | |
let message = "Found \(value) at index \(index)" | |
println(message) | |
return message | |
} | |
// Safe to use |>! if we know we'll find a value | |
find(elements, 6) |>! reportIndexOfValue(6) // "Found 6 at index 2" | |
// If left side may be nil, use |>& | |
find(elements, 3) |>& reportIndexOfValue(3) // nil | |
find(elements, 4) |>& reportIndexOfValue(4) // {Some "Found 4 at index 1"} | |
// Generic adapters | |
// The stuff below works, but it's VERY, VERY SLOW in a playground, so | |
// we've disabled it. Change the 'false' to 'true' if you want to test it. | |
#if false | |
// Rather than writing those "adapters" for the standard library | |
// filter, sorted, and map functions, we can apply a higher-level | |
// function to give them the signatures we need. | |
// Given a function with signature (A, B) -> T, return curried | |
// function with signature (B)(A) -> T | |
func pipeAdapted<A, B, T>(f: (A, B) -> T) -> (B -> A -> T) { | |
return { b in { a in f(a, b) } } | |
} | |
let pipeResultWithPipeAdapted = | |
seq |> pipeAdapted(filter)({ $0 % 2 == 0 }) | |
|> pipeAdapted(sorted)({ $1 < $0 }) | |
|> pipeAdapted(map)({ $0.description }) | |
|> String.join(", ") | |
let pipeStarResult = | |
seq |>* (filter, { $0 % 2 == 0 }) | |
|>* (sorted, { $1 < $0 }) | |
|>* (map, { $0.description }) | |
|> String.join(", ") | |
#endif |
Thanks for this, very useful gist. I came across it looking for answers why my own piping operator doesn't work correctly. It's defined the same way, but with precedence 80. It works as expected for assignments to new variables:
let x = y |> someFunction |> someOtherFunction
but fails for assignment to properties:
self.center = point |> somePointFunction
Considering that the precedence of = is said to be 90, I can't see how it would ever work actually!
In Swift 2.2 the pipe-forward operator should have precedence 95 (a bit higher than the assignment).
See Swift Standard Library Operators Reference for reference.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I derived cleaner alternatives for currying and options: https://gist.github.com/dnalot/832ab6a2ebfce04fc982#comment-1327738