Deep Dive 5 min read

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.

enespt-br