Forward Pipe vs Composition

November 14, 2019

Photo by Quinten de Graaf on Unsplash unsplash-logoQuinten de Graaf

Introduction

One thing that I found to be a point of confusion when learning F♯ is when one should use the foward pipe (|>) operator and the function composition (>>) operator. Conceptually they seem almost identical: they allow you to build an expression in which one function is called on an input, and the result is the input to the next function, and the result of the expression is the result of the second function.

Pretty much the same thing, right?

Well, almost. In the abstract, yes you’re punching an input through two functions chained together. But let’s look at the type of each operator:

val (|>) : 'T -> ('T -> 'U) -> 'U
val (>>) : ('T -> 'U) -> ('U -> 'V) -> ('T -> 'V)

Clearly these types aren’t the same. But note the key distinction: the final result of each. In the case of the composition operator (>>), the final result is a function, whereas the final result of a forward pipe (|>) expression is a value.

Let’s also have a look at the definitions of these operators, which are really quite simple (this is great because it allows you to think about the meaning rather than the syntax):

let (|>) x f = f x
let (>>) f g x = g(f(x))

Compare the types to their definitions. The forward pipe (|>) produces a value, and composition (>>) produces a function.

Concrete Examples

In most of my day-to-day F♯ programming, I have a lot more use for the |> operator. I think it’s simpler to reason about (or at least more intuitive for me), so it’s easy to write something like this:

 
// in the expressions below, m is the match value coming from the regular
// expressions, which I have omitted because they are inconsequential.
let toPascalCase (label:string) =
    _invalidCharsRx.Replace(
        _whitespaceRx.Replace(label, "_"), String.Empty)
        .Split([|'_'|], StringSplitOptions.RemoveEmptyEntries)
        |> Array.map (fun w -> 
            _startsWithLowerRx.Replace(w, (fun m -> m.Value.ToUpper())))
        |> Array.map (fun w -> 
            _upperTailRx.Replace(w, (fun m -> m.Value.ToLower())))
        |> Array.map (fun w -> 
            _lowerByNumberRx.Replace(w, (fun m -> m.Value.ToUpper())))
        |> Array.map (fun w -> 
            _upperInsideRx.Replace(w, (fun m -> m.Value.ToLower())))
        |> String.Concat

But let’s think about what’s going on here: for each element in the array produced by .Split(), we need to apply particular text replacements as defined by our regular expressions. In this version of the code, we just take the array, and pass each element through one transform, then again for the next transform, and then again for each transform. This is easy to understand, but it’s not very good code because we’re actually iterating over the array for each transformation.

What would be better would be to compose (sound familiar?) the transformations so that they can all be applied at once without requiring a full iteration of the array for each transformation. This is exactly what function composition (>>) is for:

let toPascalCase (label:string) =
    _invalidCharsRx.Replace(
        _whitespaceRx.Replace(label, "_"), String.Empty)
        .Split([|'_'|], StringSplitOptions.RemoveEmptyEntries)
        |> Array.map 
            ((fun w -> 
                _startsWithLowerRx.Replace(w, (fun m -> m.Value.ToUpper())))
             >> (fun w -> 
                    _upperTailRx.Replace(w, (fun m -> m.Value.ToLower())))
             >> (fun w -> 
                    _lowerByNumberRx.Replace(w, (fun m -> m.Value.ToUpper())))
             >> (fun w -> 
                    _upperInsideRx.Replace(w, (fun m -> m.Value.ToLower()))))
        |> String.Concat

This might be just a tad hard to read because of the clutter of the fun sytax, so let’s blow this up a little bit:

let startsWithLowerTx word = 
    _startsWithLowerRx.Replace(w, (fun m -> m.Value.ToUpper()))
let upperTailTx word = 
    _upperTailRx.Replace(w, (fun m -> m.Value.ToLower()))
let lowerByNumberTx word = 
    _lowerByNumberRx.Replace(w, (fun m -> m.Value.ToUpper()))
let upperInsideTx word = 
    _upperInsideRx.Replace(w, (fun m -> m.Value.ToLower()))

let toPascalCase (label:string) =
    _invalidCharsRx.Replace(_whitespaceRx.Replace(label, "_"), String.Empty)
        .Split([|'_'|], StringSplitOptions.RemoveEmptyEntries)
        |> Array.map 
            (startsWithLowerTx 
             >> upperTailTx 
             >> lowerByNumberTx 
             >> upperInsideTx)
        |> String.Concat

This should be much clearer. Each one of the *Tx functions above takes a string and uses a regular expression to do a particular string replacement. We use >> to compose all these transformations into a single, ordered operation, and then use that composed operation to apply our transformations to the array of strings, but with only a single iteration.

Hopefully this helps to clarify how to use these operators (composition (>>) in particular).