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 — what's happening?
  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 work with pipelines. Bare dbg consumes and returns the current pipe value, so it is useful for inspection. try usually prefixes the fallible stage it unwraps (value |> try parse()), though bare try is available when the current pipe value is already a Maybe or Result. Assertions wrap the whole pipeline from the head, so the asserted expression stays visually complete.

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

fn main(): Result<String, AssertionFailure> {
  assert Some(name) = 1
  |> find_name()

  name |> dbg

  Ok(name)
}

When the keyword validates the result of a pipeline, put it at the head:

fn main(): Result<Bool, AssertionFailure> {
  assert "Ada Lovelace"
    |> String.contains?("Ada")

  Ok(True)
}

Testing.check(...) captures an assertion result as a value:

import std/testing: Testing

fn main() {
  check_result = Testing.check(
    "Ada Lovelace"
    |> String.contains?("Ada")
  )

  check_result |> dbg

  Unit
}

if and case follow the same idea for branchy expressions:

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

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