Skip to content

Pipeline

The |> operator passes the value on the left as the first argument of the call on the right. This reverses the reading direction compared to nested calls — instead of f(g(h(x))), you write x |> h() |> g() |> f(), following the natural order of the data.

Simple pipeline and pipeline with extra arguments; splitting across multiple lines for long pipelines.

04-pipe.zolo
Playground
// Feature: Pipe operator `|>`
// Syntax: `value |> function()` — passes the LHS as the 1st arg
// When to use: chain transformations in a data-first flow, without
// nested calls. Replaces `f(g(h(x)))` with `x |> h() |> g() |> f()`,
// reading in natural order.

fn double(x: int) -> int {
  return x * 2
}

fn increment(x: int) -> int {
  return x + 1
}

fn negate(x: int) -> int {
  return -x
}

// Simple pipeline: 5 -> 10 -> 11 -> -11.
let result = 5 |> double() |> increment() |> negate()
print(result)  // -11

// Equivalent without pipe — note how it inverts when you read it:
let r2 = negate(increment(double(5)))
print(r2)  // -11

// Pipe with extra arguments — the LHS goes as the FIRST arg, the
// rest are positional.
fn add(a: int, b: int) -> int {
  return a + b
}

fn mul(a: int, b: int) -> int {
  return a * b
}

// 5 -> add(5, 10)=15 -> mul(15, 3)=45.
let calc = 5 |> add(10) |> mul(3)
print(calc)  // 45

// Pipe split across multiple lines — easier to read step by step.
let pipeline = 100
  |> add(50)
  |> mul(2)
  |> negate()
print(pipeline)  // -300

// Combining with methods via dot — string pipeline.
let clean = "  Hello, World!  ".trim().to_upper()
print(clean)  // HELLO, WORLD!

The &. operator — "tap" — calls a function with the current value but discards the return, passing the original value through. It is ideal for inserting logs or metrics in the middle of a pipeline without interrupting the flow.

Tap between |> stages to observe intermediate values without changing the result.

05-tap.zolo
Playground
// Feature: Tap operator `&.`
// Syntax: `value &. function()` — calls `function(value)` and
//         FORWARDS the original `value` (ignoring the return).
// When to use: side effects in the middle of a pipeline (logging,
// debug, metrics) without breaking the flow. It is the "spy"
// between `|>`s.

fn double(x: int) -> int {
  return x * 2
}

fn increment(x: int) -> int {
  return x + 1
}

// Function used as a tap — print is the effect, return is dropped.
fn log(x: int) -> int {
  print("  [tap] x = {x}")
  return x
}

// Pipeline with tap between stages.
let r = 5
  |> double() &. log()
  // 10
  // prints "[tap] x = 10", returns 10
  |> increment() &. log()
  // 11
  // prints "[tap] x = 11", returns 11
  |> double()
// 22
print("final={r}")

// expected:
//   [tap] x = 10
//   [tap] x = 11
//   final=22

// Tap also accepts methods via dot — useful for debugging strings.
fn show(s: str) -> str {
  print("  [str] {s}")
  return s
}

// SKIP: pipeline + tap + method call (`|> show().trim()`) is not
// yet parsed; use a temp variable instead.
let cleaned = "  hello  ".trim()
let _ = show(cleaned)
print(cleaned.to_upper())  // HELLO

// Common idiom: measure intermediate length.
// SKIP: `value &. function()` directly on an array literal/var
// currently fails parsing — use the pipeline form instead.
//
// fn count(arr: [int]) -> [int] {
//   print("  [count] len={arr.len()}")
//   return arr
// }
//
// let arr = [1, 2, 3, 4, 5]
// let final_arr = arr &. count()
// print(final_arr)

Challenge

Build a three-step pipeline with |> that: (1) multiplies by 2, (2) adds 10, and (3) negates. Add a &. log() between each step and confirm the intermediate values that are printed.

See also

enespt-br