Skip to content

Instantly share code, notes, and snippets.

@kristopherjohnson
Last active March 29, 2024 19:44
Show Gist options
  • Star 52 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save kristopherjohnson/ed97acf0bbe0013df8af to your computer and use it in GitHub Desktop.
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.
// 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
@GreatApe
Copy link

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!

@werediver
Copy link

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