Pipe and Composition
The scenario
You have a list of numbers and you want to: keep the odd ones, square them, then sum the result. In C#, without fluent LINQ, this is easily written as calls nested inside one another:
var result = Enumerable.Sum(Enumerable.Select(Enumerable.Where(numbers, x => x % 2 != 0), x => x * x));
To read this code, you have to decode it from the inside out: first Where, then Select, then Sum — while the logical order of the operations actually goes left to right. LINQ's fluent syntax (numbers.Where(...).Select(...).Sum()) already fixes this problem on the C# side. F# offers an equivalent mechanism, but one that isn't limited to an object's methods: the pipe operator, |>.
The pipe operator: |>
|> takes a value on its left and a function on its right, and applies the function to the value. Its definition fits on a single line in F#'s standard library:
let (|>) x f = f x
This lets you chain transformations in the order they actually happen, left to right (or top to bottom):
let numbers = [ 1; 2; 3; 4; 5 ]
let result =
numbers
|> List.filter (fun x -> x % 2 <> 0)
|> List.map (fun x -> x * x)
|> List.sum
Notice the list syntax along the way: elements separated by semicolons inside brackets, [ 1; 2; 3; 4; 5 ], not by commas like in a C# new List<int> { 1, 2, 3, 4, 5 } — F# reserves the comma for another use (grouping several values into one, which you'll run into later in this path).
Also notice fun x -> x % 2 <> 0: this is an F# anonymous function, the direct equivalent of the C# lambda x => x % 2 != 0. The fun keyword introduces the parameters (here, just one, x), and -> replaces C#'s => to separate the parameters from the function body. You'll write one of these every time you pass a small function directly to List.filter or List.map, without giving it a name with let beforehand. Another syntax difference along the way: F# writes "not equal to" as <>, where C# writes !=.
Each line reads like a step: "starting from numbers, keep the odd ones, square them, sum them up." It's exactly the same computation as the nested C# example above, but written in the order you actually think about it.
List.filter and List.map work like their LINQ equivalents Where and Select; List.sum computes the sum of a list of numbers, provided the element type supports the + operator — which is the case for int here.
The composition operator: >>
The pipe chains calls — it needs a starting value to work. Composition, on the other hand, builds a new function out of two existing functions, without ever supplying them a value: it's an operation on functions, not on data.
let addOne x = x + 1
let timesTwo x = 2 * x
let addOneThenDouble = addOne >> timesTwo
addOneThenDouble is a fully-fledged function that hasn't computed anything yet — it's only applied to a value once you call it:
addOneThenDouble 3
Here, 3 first goes through addOne (result 4), then that result goes through timesTwo (result 8). >> therefore reads, like |>, left to right: the function on the left runs first.
The difference in one sentence
|> takes a value and a function, and returns a value. >> takes two functions, and returns a function. Nothing else to remember for now — the most common confusion is trying to use >> with a value instead of a function on the left, or the reverse with |>: the compiler will refuse either way, since the types don't match.
There's no direct equivalent to >> in C# — combining two Func<T,T> into a single function requires writing your own wrapping function or reaching for Func.Compose-like helpers that the language itself doesn't provide; >> does this natively.
Exercise 1 — rewrite a nested call as a pipeline
Here's a second nested example, this time to transform a list of strings: keep the ones with more than 3 characters, uppercase them.
var result = Enumerable.Select(Enumerable.Where(words, w => w.Length > 3), w => w.ToUpper());
Open dotnet fsi and write the F# pipeline version, starting from this list:
let words = [ "cat"; "horse"; "zebra"; "rabbit"; "dog" ]
Write your pipeline using List.filter and List.map, then run it with printfn "%A" result;; to display the result. printfn is the F# equivalent of Console.WriteLine, with a formatting syntax inherited from C's printf; %A is an F#-specific specifier that displays any value — list, tuple, record — in a readable form, with no conversion code to write yourself.
Expected output:
["HORSE"; "ZEBRA"; "RABBIT"]
If you get ["CAT"; "HORSE"; "ZEBRA"; "RABBIT"; "DOG"], the filter condition wasn't applied before the uppercasing — check the order of your two steps in the pipeline.
Exercise 2 — compose two short functions
Write two tiny functions:
let square x = x * x
let addTen x = x + 10
Then, without using |>, compose them with >> into a single function named squareThenAddTen, and apply it to 4.
Expected output:
val squareThenAddTen: (int -> int)
val it: int = 26
(4 squared gives 16, plus 10 gives 26.) If you get 196 instead (addTen 4 gives 14, then square 14 gives 196), the order of the two functions in the composition is reversed — remember that >> runs the left-hand function first.
What you just did
You replaced a nested function call, hard to read in the order the computation actually happens, with a |> pipeline that follows that order literally. You also built a new function from two existing functions with >>, without ever supplying them a value before combining them — an operation with no direct, native equivalent in C#.
In the next module, you'll leave data transformation behind to tackle decision-making: how match replaces if/else and switch to branch on the shape of a value, in particular on a tuple.
Check your understanding
On numbers |> List.filter (fun x -> x % 2 <> 0) |> List.map (fun x -> x * x) |> List.sum, a colleague suggests swapping the last two steps: numbers |> List.filter (fun x -> x % 2 <> 0) |> List.sum |> List.map (fun x -> x * x). What happens?
With let addOne x = x + 1 and let timesTwo x = 2 * x, what's the difference between addOne >> timesTwo and addOne |> timesTwo?
You define let square x = x * x and let addTen x = x + 10, then let addTenThenSquare = addTen >> square. What's the result of addTenThenSquare 4?
Want to hear about the next modules?
The Academy stays free and open-access, no sign-up required. If you'd just like to be notified by email when a new module ships, here you go — no obligation, unsubscribe anytime with one click.