-
-
Save kristopherjohnson/ed97acf0bbe0013df8af to your computer and use it in GitHub Desktop.
// 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 |
If you want this F#-style |> operator in Swift, please dupe my radar: http://www.openradar.me/radar?id=5837161337192448
There are no type constraints though. Is it going to be safe?
Yes, it's safe. All it does is convert x |> f
to f(x)
, and convert something like x |> f |> g |> h
to h(g(f(x)))
, so all of the types and constraints of those functions are still in effect.
I wrote a blog entry describing this in detail: http://undefinedvalue.com/2014/07/13/fs-pipe-forward-operator-swift
Just updated this for Xcode 6 beta 4. Added "public" to the operator definition, and had to change the return types for the map and filter functions.
Hi, I'm using some of these ideas here: https://github.com/kareman/SwiftShell/blob/master/SwiftShell/Pipes.swift , but I kept the names of the curried functions the same as the original ones, I think the piped code looks more natural this way.
I derived cleaner alternatives for currying and options: https://gist.github.com/dnalot/832ab6a2ebfce04fc982#comment-1327738
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.
What am I trying to do here? In F#, there is a "pipe-forward" operator (|>) that applies the function on its right side to the value on the left side. This makes it easy to write complex expressions that are evaluated from left-to-right/top-to-bottom, like this:
or like this:
instead of traditional inside-out/right-to-left function-application notation, like this
Many people find the left-to-right order more readable, especially in cases like this where the filters are higher-order functions that take parameters.
Thanks to help from @gregtitus on Twitter: https://twitter.com/gregtitus/status/475025408308805632, now I can do this in Swift.
In F#, this operator makes it possible for the user to type the argument and the |> operator, and then IntelliSense will automatically display a list of functions that can be applied to that argument type. Maybe we can talk Apple into adding this feature to Xcode.