Skip to content

Pipes

x |> f() is f(x). The piped value goes into the first argument of the call on its right; any additional arguments come after. Pipes let you describe a computation as a top-to-bottom flow rather than an inside-out chain of nested calls — which is how most idiomatic Nomi reads.

The simplest pipe is one stage. When the call has more than one argument, the piped value fills the first slot and the rest are written after:

fn double(x: Int): Int {
  x * 2
}

fn add(x: Int, y: Int): Int {
  x + y
}

fn main(): Int {
  5 |> double() |> dbg

  5 |> add(3) |> dbg
}

Longer chains stack one stage per line — the data flows top-to-bottom and each stage gets its own row:

fn main(): List<Int> {
  [1, 2, 3]
  |> Iter.map(|n| n * n)
  |> Iter.to_list()
  |> dbg
}

Pipes read most clearly when the chain starts from concrete data and ends at where it’s used. Compare:

fn main(): Int {
  // inside-out
  dbg Iter.count(Iter.filter([1, 2, 3, 4, 5], |n| n > 2))
  // pipeline — data first, transformations next, output last
  [1, 2, 3, 4, 5]
  |> Iter.filter(|n| n > 2)
  |> Iter.count()
  |> dbg
}

Both produce 3; only the second reads top-to-bottom.

The piped value goes into the first argument by default. To pipe into a different position, write _ where the piped value should land:

fn divide(x: Int, y: Int): Int {
  x / y
}

fn main(): Int {
  10 |> divide(100, _) |> dbg
}

That _ becomes divide(100, 10), which is 100 / 10.

A pipe stage can be a lambda. Use this when the next step is a small inline expression rather than a named function call:

fn find_name(id: Int): Maybe<String> {
  case id {
    1 -> Some("Ada")
    _ -> None
  }
}

fn main(): Bool {
  1
  |> find_name()
  |> |name| name == Some("Ada")
  |> dbg
}

Expression keywords can also be pipeline stages. Bare dbg inspects the current value; try unwraps Maybe and Result stages; assertions usually sit at the head of test pipelines.

try short-circuits on None or Err:

fn main(): Maybe<Int> {
  "  42  "
  |> String.trim()
  |> try String.to_int()
  |> Some()
  |> dbg
}

Assertions keep the subject visually complete:

test "pipeline result is true" {
  assert "  Ada Lovelace  "
    |> String.trim()
    |> String.contains?("Ada")
}

if and case follow the same idea for branchy expressions:

fn main(): String {
  "Ada"
  |> if String.contains?("A") {
      "initialed"
    } else {
      "plain"
    }
  |> dbg

  Some("Ada")
  |> case {
      .Some(name) -> "named ${name}"
      .None -> "missing"
    }
  |> dbg
}

The next chapter — Scalars & Strings — uses pipes from the first example onward.