The Pipe Operator
How |> turns nested function calls into a readable left-to-right pipeline, and the conventions that make it work well.
2 min read
Elixir code leans heavily on transforming data through a series of function calls, and without help, that quickly turns into deeply nested, hard-to-read calls. The pipe operator, |>, solves this directly.
The problem it solves
Say you want to take a string, trim whitespace, split it into words, and count them. Without piping, you write it inside-out:
length(String.split(String.trim(" hello world ")))
# => 2To read this, you have to start from the innermost call and work outward — the opposite of the order the operations actually happen in.
Piping rewrites this left to right
" hello world "
|> String.trim()
|> String.split()
|> length()|> takes the result of the expression on its left and inserts it as the first argument of the function call on its right. So value |> foo(a, b) is exactly equivalent to foo(value, a, b). The pipeline above reads in the same order the computation actually happens: trim, then split, then count.
Why the first argument matters
This is precisely why Elixir's standard library consistently puts the "primary" piece of data being operated on as the first argument of every function — String.trim/1, String.split/1, Enum.map/2, Map.put/3, and so on. That consistency isn't an accident; it's what makes piping through the standard library feel natural instead of fighting the argument order.
%{name: "ada", role: "admin"}
|> Map.put(:active, true)
|> Map.update!(:name, &String.capitalize/1)%{name: "Ada", role: "admin", active: true}Here, the map flows through Map.put/3 and Map.update!/3 as the first argument to each, exactly the shape those functions expect.
Writing your own pipe-friendly functions
If you write custom functions meant to be piped, follow the same convention — the data being transformed goes first:
defmodule TextTools do
def shout(text) do
String.upcase(text) <> "!"
end
def truncate(text, length) do
String.slice(text, 0, length)
end
end
"hello world"
|> TextTools.truncate(5)
|> TextTools.shout()
# => "HELLO!"Because text is the first parameter in both truncate/2 and shout/1, they slot into a pipeline without any awkward rearranging.
When not to force it
Piping is a readability tool, not a rule to apply everywhere. A single function call, or two calls where the second doesn't naturally take the first's output as its main argument, usually reads better without |>:
# Unnecessary -- adds a pipe for a single call
5 |> Kernel.+(3)
# Clearer as-is
5 + 3Used well, though, pipelines are one of the most immediately recognizable and beloved parts of Elixir's style — they turn a tangle of nested calls into something that reads almost like a sentence describing a series of steps.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.