F# is very exciting and fun language to learn. It contains pipe and composition operators which allows you to write less code with better conciseness. In addition to familiar prefix and postfix operators it also comes with the “infix” operator. The beauty of it is that you can define your own infix operators and succinctly express business logic in your F# code.
Prefix, infix and postfix 👾
As an example of prefix operators we can define any regular function:
let times n x = x * n
and call this function with a prefix notation:
times 3 3 // val it : int = 9
In F# vast majority of primitives are functions, just like in pure OOP language everything is an object. So you can also call multiplication operator as a function:
(*) 3 3 // val it : int = 9
which gives the same result as in the previous code snippet.
Postfix operators is not something you often use and mostly comes with built-in keywords:
type maybeAString = string option // built-in postfix keyword type maybeAString2 = Option<string> // effectively same as this // Usage let s:maybeAString = Some "Ninja in the bushes!" let s2:maybeAString2 = None
But most interesting one is infix operator. As you already could guess, infix operator should be placed between two operands. Everyone did some math in school and wrote something similar to:
3 * 3 // val it : int = 9
Not surprisingly it is something you use without even thinking. Now, let’s define few custom functions:
let (^+^) x y = x ** 2. + y ** 2. // sum of the square of two numbers let (^^) x y = x ** y // returns x to the power of y
And use it with an infix operator:
3. ^+^ 3. // val it : float = 18.0 3. ^^ 3. // val it : float = 27.0
Note that we can also use it with a prefix notation just as a regular functions:
(^+^) 3. 3. // val it : float = 18.0 (^^) 3. 3. // val it : float = 27.0
Of course infix syntax looks much more succinct in that case.
Pipe, compose and mix 🔌
The king among F# operators is a pipe operator (|>). It allows you to express function composition in a readable way. Function application is left associative, meaning that evaluating x y z is the same as evaluating (x y) z. If you would like to have right associativity you can use explicit parentheses or pipe operator:
let fun x y z = x (y z) let fun x y z = y z |> x // forward pipe operator let fun x y z = x <| y z // backward pipe operator
Okay. As you see there two flavors of pipe operators: forward and backward. Here the definition of forward pipe operator:
let (|>) x f = f x
Just as simple as that: feeding the argument from the left side (x) to function (f). The definition of the backward pipe operator is:
let (<|) x f = x f
You may wonder why it is needed and what is benefit of using it? You will see example later in this post.
So how we can apply pipe operators in practice? Here examples:
let listOfIntegers = [5;6;4;3;1;2] listOfIntegers |> List.sortBy (fun el -> abs el) // val it : int list = [1; 2; 3; 4; 5; 6] // Same as List.sortBy (fun el -> abs el) listOfIntegers
It shines when you have long list of functions you need to compose together:
text.Split([|'.'; ' '; '\r'|], StringSplitOptions.RemoveEmptyEntries) |> Array.map (fun w -> w.Trim()) |> Array.filter (fun w -> w.Length > 2) |> Array.iter (fun w -> ...
The backward pipe operator could be useful in some cases to make your code looks more English-like:
let myList =  myList |> List.isEmpty |> not // Same as above but looks prettier myList |> (not << List.isEmpty)
Composition operator could also be forward (>>) and backward (<<) and it also used for composing functions. Unlike pipe operator, result of execution compose will be a new function.
Definition of composition operators:
let (>>) f g x = g ( f(x) ) let (<<) f g x = f ( g(x) )
let add1 x = x + 1 let times2 x = x * 2 let add1Times2 = (>>) add1 times2 add1Times2 3 // val it : int = 8
Which we could re-write like this:
let add x y = x + y let times n x = x * n let add1Times2 = add 1 >> times 2 add1Times2 3 // val it : int = 8
In both examples it relies on core concept of partial application, that is when one argument baked-in in functions add1 and times2 but left second argument free so that it will be passed on function invocation by the user.
As long as input and outputs of functions involved in composition match, any kind of value could be used
Same example with backward composition operator gives different result because functions composed in the opposite order:
let add x y = x + y let times n x = x * n let times2Add1 = add 1 << times 2 times2Add1 3 // val it : int = 7
Have fun 😝
Now a small exercise for you. What will be outcome of all these expressions? 🤔 :
3 * 3 (*) 3 3 3 |> (*) 3 3 |> (*) <| 3
What about that one:
let ninjaBy3 = 3 * 3 |> (+) ninjaBy3 5
Try it yourself. Leave comments and have fun!