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 pipe operator
Section titled “The pipe operator”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 start from data
Section titled “Pipes start from data”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 _ placeholder
Section titled “The _ placeholder”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.
Lambda stages
Section titled “Lambda stages”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
}
Keyword stages
Section titled “Keyword stages”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.