Why the Pipe Operator Exists
Deeply nested function calls like String.upcase(String.trim(str)) force a reader to parse from the innermost call outward to understand what happens first. The pipe operator |> rewrites this as str |> String.trim() |> String.upcase(), taking the expression on its left and inserting it as the first argument of the call on its right, so the code reads top-to-bottom in the order operations actually execute.
Cricket analogy: Reading String.upcase(String.trim(str)) is like a scorecard reporting 'the century scored off the last ball of the innings that followed the rain delay after the toss' in one breath, whereas str |> String.trim() |> String.upcase() reads like commentary: toss, then rain delay, then innings, then century.
How Piping Inserts the First Argument
The pipe always drops the left-hand value into the first argument slot of the call on the right, no matter how many other arguments that call takes -- list |> Enum.map(&(&1 * 2)) compiles to Enum.map(list, &(&1 * 2)). Parentheses around the remaining arguments are required in a pipe chain, and this first-argument rule is exactly why Elixir's standard library is designed so the 'subject' of an operation (the list, the string, the struct) is conventionally the first parameter.
Cricket analogy: The pipe always fills the first argument slot, like a coach who always assigns the incoming net bowler to face the first batsman in the queue, no matter how many other parameters -- like target line or length -- the drill also specifies.
When Piping Doesn't Fit Naturally
The pipe can't feed a value into an operator like +, -, or >, because those are infix operators, not ordinary functions with a first-argument slot -- runs |> > 50 is a syntax error, and runs > 50 must be written normally. Similarly, piping into an anonymous function requires parentheses after the closing end, as in value |> (fn x -> x * 2 end).(), since the dot-call syntax for anonymous functions doesn't disappear just because the value arrived via a pipe.
Cricket analogy: You can't pipe a score into a comparison like runs |> > 50 any more than a scoreboard can infer 'greater than' from the number 50 alone -- you write runs > 50 normally, the way an umpire states a comparison outright rather than receiving it as a delivery.
Piping into an anonymous function or a captured operator needs explicit parentheses: value |> (fn x -> x * 2 end).() or value |> (&(&1 * 2)).(). Forgetting the trailing .() (or the wrapping parentheses) produces a confusing syntax error, since Elixir needs to see clearly where the anonymous function literal ends and the invocation begins.
Building and Debugging a Real Pipeline
A realistic pipeline chains several transformations end to end, such as text |> String.downcase() |> String.split() |> Enum.frequencies() to count word occurrences in a block of text. To debug a long pipeline, insert |> IO.inspect(label: "after split") at any point -- IO.inspect/2 prints its argument and returns it unchanged, so the pipeline keeps flowing exactly as before, just with a visible checkpoint.
Cricket analogy: Piping raw commentary text through String.downcase() |> String.split() |> Enum.frequencies() to find the most-repeated word is like a broadcaster running ball-by-ball transcripts through a word-cloud generator, and inserting IO.inspect(label: "after split") midway is like pausing to check the over-by-over tally before the innings total is calculated.
" Hello Elixir World "
|> String.trim()
|> String.downcase()
|> String.split()
|> Enum.frequencies()
|> IO.inspect(label: "word frequencies")
# word frequencies: %{"elixir" => 1, "hello" => 1, "world" => 1}
[1, 2, 3, 4, 5, 6, 7, 8]
|> Enum.filter(&(rem(&1, 2) == 0))
|> Enum.map(&(&1 * 10))
|> Enum.sum()
# => 200
# dbg/1 prints every step of a pipeline, with source location, automatically:
[1, 2, 3]
|> Enum.map(&(&1 * 2))
|> Enum.sum()
|> dbg()
Since Elixir 1.10, the dbg/1 macro is a purpose-built alternative to sprinkling IO.inspect/2 calls through a pipeline. Wrapping the whole pipeline in dbg() (or piping the final result into it) prints every intermediate step along with the file and line number, then returns the pipeline's final value unchanged -- ideal for debugging in iex without editing every line.
- The pipe operator |> inserts its left-hand expression as the first argument of the call on its right.
- Pipelines read top-to-bottom in execution order, replacing deeply nested, inside-out function calls.
- Parentheses are required around a piped-into function's remaining arguments.
- Operators like +, -, and > cannot be piped into directly since they aren't ordinary functions with a first-argument slot.
- Piping into an anonymous function needs explicit call syntax, e.g. value |> (fn x -> x * 2 end).().
- IO.inspect/2 with a :label option is a common way to peek at an intermediate pipeline value without altering the result.
- The dbg/1 macro (Elixir 1.10+) prints every step of a pipeline automatically, including source location.
Practice what you learned
1. What does `str |> String.trim() |> String.upcase()` do?
2. In `list |> Enum.map(&(&1 * 2))`, where does list get inserted?
3. Why can't you write `runs |> > 50`?
4. What is `IO.inspect(label: "after filter")` commonly used for in a pipeline?
5. What does the dbg/1 macro add over manually inserted IO.inspect calls?
Was this page helpful?
You May Also Like
Recursion and Enum
Explore how Elixir uses recursion for iteration and how the Enum and Stream modules provide higher-level functions built on that foundation.
Functions and Modules in Elixir
Learn how Elixir organizes code into modules and functions, covering named functions, arity, default arguments, private functions, module attributes, and anonymous functions.
Guards and case in Elixir
Learn how Elixir's case expression and guard clauses let you branch on pattern matches with additional boolean conditions, and when to reach for cond instead.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics