Categories
Tech

F# Friday – Pipeline Operators

I’ve already used the pipeline operator ( |> ) in previous F# Friday posts, but it occurred to me that it and its comrades are worth a discussion of their own.

F#’s convention in collection modules is to make the collection the last argument to any function.

let hs = List.filter 
           (fun (s:string) -> s.StartsWith("h"))
           ["hi"; "di"; "ho"]
// val hs : string list = ["hi"; "ho"]

Simple enough, but it gets a little hairy when you want to chain operations together. Let’s say that you want to (1) perform a map operation on all the elements in a list, (2) filter out some elements, (3) take the first three elements, and then finally (4) multiply them all together. You can pack it all into one expression, but it’s not pretty:

let p = Seq.reduce (*)
         (Seq.truncate 3
           (List.filter (fun n -> n % 2 = 1)
             (List.map (fun n -> (3 * n)) [1..10])))
// val p : int = 405

The parentheses start to stack up, and to follow what’s going on, you’re forced to do the opposite of what comes naturally: You have to start with the end of the expression and work your way backward to the beginning.

What is this, Clojure? (Wink, wink, nudge, nudge, say no more.)

You can improve upon the above by assigning the result of each step to a temporary variable:

let mapped = List.map (fun n -> (3 * n)) [1..10]
let filtered = List.filter (fun n -> n % 2 = 1) mapped
let truncated = Seq.truncate 3 filtered
let reduced = Seq.reduce (*) truncated
// val reduced : int = 405

That’s … better, but it could be better still. What pattern do we notice about each step? The result of each step becomes the last input of the next step. What if there were a way to forward the result of each step to the last input of the step that follows it? That way we could read each step of the process, top-to-bottom and left-to-right, in the order that it occurs.

Enter the Pipeline Operator

Specifically the forward pipeline operator:

let p = List.map (fun n -> (3 * n)) [1..10]
        |> List.filter (fun n -> n % 2 = 1)
        |> Seq.truncate 3
        |> Seq.reduce (*)
// val p : int = 405

Ah, that’s much better. The result of the expression on each line feeds directly into the next line, becoming its final input. We can actually make one more little tweak:

let p = [1..10]
        |> List.map (fun n -> (3 * n))
        |> List.filter (fun n -> n % 2 = 1)
        |> Seq.truncate 3
        |> Seq.reduce (*)
// val p : int = 405

That puts the input on its own line, so that it’s clear what the starting state is, and then there’s nothing on each line that follows but the operation to be performed on the input at that stage of the process.

Two Are Better than One

There is also a two-element version of the pipeline operator. If you want to use it to pass two values to a two-input function, just wrap those inputs in a two-element tuple (also known as a pair or a couple):

let subtract a b = a - b
let diff = subtract 17 11
let diff2 = (17, 11) ||> subtract
// val diff : int = 6
// val diff2 : int = 6

The use case for ( ||> ) is when you need to chain calls together that take two arguments and return a couple. It’s a contrived example, but you could calculate Fibonacci numbers this way:

let fib m n = n, m + n
let f = (1, 1)
        ||> fib
        ||> fib
        ||> fib
        ||> fib
// val f : int * int = (5, 8)

A Threefold Cord

There is even a three-element version: ( |||> ). You could take a three-element position tuple (or a triple) containing a north-south element, an east-west element, and an up-down element and move it around with functions that take three arguments and return a triple:

let north ns ew ud = (ns + 1, ew,     ud)
let south ns ew ud = (ns - 1, ew,     ud)
let east  ns ew ud = (ns,     ew + 1, ud)
let west  ns ew ud = (ns,     ew - 1, ud)
let up    ns ew ud = (ns,     ew,     ud + 1)
let down  ns ew ud = (ns,     ew,     ud - 1)
let p3 = (0,0,0)
         |||> north |||> north |||> north
         |||> west
         |||> down |||> down
// val p3 : int * int * int = (3, -1, -2)

Do I Hear Four?

Alas, if you need to pass a four-element tuple (a quad) to a four-input function, there is no pre-defined pipeline operator that meets your needs. And probably for good reason, frankly. But we throw caution to the wind around here: Let’s just define our own!

let (||||>) (a, b, c, d) f = f a b c d

Now you can extend that position triple with a fourth element that accumulates the distance covered with each move:

let (||||>) (a, b, c, d) f = f a b c d
let north4 ns ew ud Δ = (ns + 1, ew,     ud,     Δ + 1)
let south4 ns ew ud Δ = (ns - 1, ew,     ud,     Δ + 1)
let east4  ns ew ud Δ = (ns,     ew + 1, ud,     Δ + 1)
let west4  ns ew ud Δ = (ns,     ew - 1, ud,     Δ + 1)
let up4    ns ew ud Δ = (ns,     ew,     ud + 1, Δ + 1)
let down4  ns ew ud Δ = (ns,     ew,     ud - 1, Δ + 1)
let p4 = (0,0,0,0)
         ||||> north4 ||||> north4 ||||> north4
         ||||> west4
         ||||> down4 ||||> down4
// val p4 : int * int * int * int = (3, -1, -2, 6)

… We Came In

Finally, there are also one-, two-, and three-element versions of the backward pipeline operator. A backward pipeline operator sends the result of its right-hand expression to the function on the left-hand side as its final input value:

let weapon = Some 6
printfn "%s" <| match weapon with
                | Some d -> 
                  sprintf "Your weapon does %d points of damage" d
                | None -> "Better run for the hills"
// Your weapon does 6 points of damage

The backward pipeline operators see less use than the forward operators, and you can probably see why. What’s the point? The reason for the forward pipeline is to transplant the result of an expression from the left-hand side of the function call to the right. Well, if you’re already on the right-hand side of the function call, why would you need another operator? Dave Fancher in The Book of F# puts it this way:

Because it changes precedence within an expression, the backward pipelining operator is sometimes used as a replacement for parentheses.

As you can see in the example above, you are able to put a complex expression on the right-hand side of the backward pipeline operator. The whole match expression is evaluated first and then passed to printfn as its final argument.

This Is Where …

Even though the backward pipeline operator can eliminate some parentheses, sometimes you still need some parentheses to force the right precedence. For instance, this doesn’t work:

let s = String.concat " " <| Seq.append ["DO"] <| ["MI";"SOL"]
--------^^^^^^^^^^^^^^^^^
// error FS0001: Type mismatch. Expecting a
//     'a -> 'b -> 'c    
// but given a
//     'a -> string    
// The type ''a -> 'b' does not match the type 'string'

This happens because, while backward pipelining allows you to evaluate the right-hand expression first, the compiler still evaluates chains of backward pipelines from left to right. In other words, the compiler sees the above expression as this:

let s = (String.concat " " <| Seq.append ["DO"]) <| ["MI";"SOL"]

Bend the compiler to your will with this:

let s = String.concat " " <| (Seq.append ["DO"] <| ["MI";"SOL"])
// val s : string = "DO MI SOL"

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.