100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

The Pipe Operator

Understand how Elixir's |> pipe operator chains function calls into readable, left-to-right data transformation pipelines.

Core ConceptsBeginner7 min readJul 10, 2026
Analogies

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.

elixir
"  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

Was this page helpful?

Topics covered

#Programming#ElixirStudyNotes#ThePipeOperator#Pipe#Operator#Exists#Piping#StudyNotes#SkillVeris#ExamPrep