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
@kristopherjohnson
Copy link
Author

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:

let rows = resultCollection |>  filterBy(selectedCityName) |> orderBy(selectedColumn)

or like this:

let rows = resultCollection
    |> filterBy(selectedCityName)
    |> orderBy(selectedColumn)

instead of traditional inside-out/right-to-left function-application notation, like this

let rows = orderBy(selectedColumn)(filterBy(selectedCityName)(resultCollection))

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.

@kristopherjohnson
Copy link
Author

If you want this F#-style |> operator in Swift, please dupe my radar: http://www.openradar.me/radar?id=5837161337192448

@isaka
Copy link

isaka commented Jun 11, 2014

There are no type constraints though. Is it going to be safe?

@kristopherjohnson
Copy link
Author

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.

@kristopherjohnson
Copy link
Author

I wrote a blog entry describing this in detail: http://undefinedvalue.com/2014/07/13/fs-pipe-forward-operator-swift

@kristopherjohnson
Copy link
Author

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.

@kristopherjohnson
Copy link
Author

@kareman
Copy link

kareman commented Oct 16, 2014

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.

@texastoland
Copy link

I derived cleaner alternatives for currying and options: https://gist.github.com/dnalot/832ab6a2ebfce04fc982#comment-1327738

@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