The Pipe Operator: Functional Flow Without the Boilerplate
A deep dive into the |> operator — how it works, why it matters, and real-world examples of functional pipelines in Zolo.
The Pipe Operator: Functional Flow Without the Boilerplate
The pipe operator |> is one of Zolo's most beloved features. If you've used Elixir, F#, or Hack, you'll feel right at home. If not — let me show you why it changes everything.
The Problem with Nested Calls #
Consider a classic transformation pipeline: take a list of words, filter the short ones, capitalize them, and join with a comma.
In most languages:
// JavaScript — hard to read inside-out
const result = words
.filter(w => w.length > 3)
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
.join(', ')
-- Lua — even worse
local result = table.concat(
map(capitalize,
filter(function(w) return #w > 3 end, words)
), ', '
)
The Pipe Solution #
let result = words
|> Array.filter(|w| String.len(w) > 3)
|> Array.map(|w| String.capitalize(w))
|> Array.join(", ")
print(result)
Read it top-to-bottom, left-to-right. Each step is a transformation. No nesting, no temporary variables.
How It Works #
The |> operator passes the left-hand value as the first argument to the right-hand function call:
x |> f() // equivalent to: f(x)
x |> f(y) // equivalent to: f(x, y)
x |> f(y, z) // equivalent to: f(x, y, z)
This is different from method chaining (.map().filter()) because it works with any function, not just methods on a type.
Real-World Pipeline #
Here's a data processing pipeline that parses CSV, filters rows, and aggregates:
fn parse_row(line: str) -> [str] {
return line |> String.split(",") |> Array.map(|s| String.trim(s))
}
fn is_valid(row: [str]) -> bool {
return Array.len(row) == 3 && row[0] != ""
}
let summary = raw_csv
|> String.lines()
|> Array.skip(1) // skip header
|> Array.filter(is_valid)
|> Array.map(parse_row)
|> Array.map(|row| {
name: row[0],
score: Int.parse(row[1]) ?? 0,
grade: row[2],
})
|> Array.filter(|r| r.score >= 60)
|> Array.len()
print("Passing students: {summary}")
Composing Functions #
Pipe shines with function composition:
fn double(x: int) -> int { x * 2 }
fn increment(x: int) -> int { x + 1 }
fn square(x: int) -> int { x * x }
// Clear intent, no nesting
let result = 5
|> double() // 10
|> increment() // 11
|> square() // 121
print(result) // 121
Pipe with Iterators #
The pipe operator is especially powerful with lazy iterators:
// Find the sum of squares of all odd numbers up to 1000
let answer = 0..
|> Iter.filter(|x| x % 2 != 0)
|> Iter.map(|x| x * x)
|> Iter.take_while(|x| x < 1_000_000)
|> Iter.fold(0, |acc, x| acc + x)
print(answer)
This evaluates lazily — the infinite range 0.. is never fully computed. Elements flow through the pipeline one at a time.
Conclusion #
The pipe operator is a small syntax addition with outsized impact on readability. It turns inside-out function calls into natural pipelines, and pairs beautifully with Zolo's iterator library.
Try it in the Playground — paste any example and see it run.