F# Pipelining vs. Composition

Rolling onto my current project five months ago, I got my first look at a large enterprise codebase written in F# (or any codebase written in F#, for that matter). This took some getting up to speed because F# looks wildly different than any other language I’d come across before. One of the first questions I remember asking my pair was, “What’s the difference between |> and >>?”

These are called the pipeline operator and the composition operator, respectively. They are as ubiquitous in F# as parentheses in LISP or curly braces in C styles languages. It took a bit of reading and experimentation to sort out the nuts-and-bolts difference between these F# operators. It took a bit longer to sort out when it’s most appropriate to use one or the other.

The Basic Difference

F# is a functional programming language that puts a huge emphasis on static types. With that in mind, let’s take a look at the type signature of the pipeline operator (which, like many things in F#, is really just a function):

(|>) // : ('a -> ('a -> 'b) -> 'b)

So, the pipeline operator is a function that takes a value of type 'a and a function of type 'a -> 'b and returns a value of  'b. Pretty simple. What about composition?

(>>) // : (('a -> 'b) -> ('b -> 'c) -> 'a -> 'c)

Composition is a function that takes a function of 'a -> 'b and a function of 'b -> 'c and returns a function of 'a -> 'c Those seem like very different signatures, so why the confusion?

How They’re Used

The confusion comes from the fact that the two operators often show up in what looks like the same context in code: right between two function names. For instance:

let add1 x = x + 1
let times2 x = x * 2
let subtract20 x = x - 20

// pipeline
100 |> add1 |> times2 |> subtract20 // 182

// composition
let addTimesSubtract = add1 >> times2 >> subtract20 // : int -> int

Notice that in both cases, the operators sit between add1, times2, and subtract20. That’s because they both allow for constructing code that behaves like a pipeline of functions, running each in turn from left to right. The difference is that, for |>, there needs to be a value on the left and a function on the right, while >> wants a function on either side.

So why have both? The pipeline example above could easily be re-written like so:

100 |> (add1 >> times2 >> subtract20) // 182

And the composition example could be transformed into:

let addTimesSubtract2 x = x |> add1 |> times2 |> subtract20 // : int -> int

You can use the two operators nearly interchangeably. The difference is that the pipeline operator is generally used in the context of values, while composition deals with functions. If you have a value that needs to be transformed into another value in a one-off way, say, in the body of a larger function, that’s a good time for a pipeline. If you want to bundle up that transformation into a reusable chunk of behavior, that sounds like function composition (like addTimesSubtractabove).

Composition is nice because it allows you to compose functionality together without explicitly calling out the arguments. For instance, it’s nicer to write this:

[100;200;300] |> List.map (add1 >> times2 >> subtract20)

Than this:

[100;200;300] |> List.map (fun x -> x |> add1 |> times2 |> subtract20)

Writing code compositionally can cut away some of the noise that crops up from defining throwaway names like x that don’t add any meaning to the code. On the other hand, that transformation might be part of a more complex business rule. In that case, it might be more readable to use the lambda + pipeline approach and explicitly name the variable.

F# Operators for More Readable Code

If you’re new to F#, you’re definitely going to see plenty of both the pipeline and composition operators. Most of the time, the usage of each will look nearly identical. This might lead you to wonder about the difference between the two. The good news is that, because the types of the two operators are different, and because F# is so strict about types, it’s not possible to use the wrong one dangerously. Choosing between the two, like many things, is about finding which approach leads to more readable code.