Forward Pipe vs Composition
November 14, 2019Introduction
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).