Pipelines
Pipelines are the most common way to structure everyday Lutra code. They let you read a transformation from left to right: start with a value, apply one step, then apply the next.
Define small functions¶
A function takes input values and returns an output value.
func greet(name: text) -> f"Hello, {name}!"
func add(a: int32, b: int32) -> a + b
func main() -> {greet("Ada"), add(3, 4)}
Functions can infer their return type when it is obvious. You can also annotate it explicitly.
func is_even(x: int32): bool -> x % 2 == 0
In practice, Lutra code often uses:
- small named helper functions,
- inline functions inside pipelines,
- tuples to keep related results together.
Use constants for fixed values¶
Constants are useful for fixed values that do not require computation.
const tax_rate: float64 = 0.2
func with_tax(price: float64) -> price * (1.0 + tax_rate)
Use func when you need computation. Use const when you need a reusable
value.
Read pipelines from left to right¶
A pipe sends the value on the left into the expression on the right.
func add_one(x: int32) -> x + 1
func double(x: int32) -> x * 2
func main() -> 10 | add_one | double
This is equivalent to:
func main() -> double(add_one(10))
Pipelines become more useful as soon as you have more than one or two steps. They make the flow of data visible.
Use inline functions inside pipelines¶
You can write a function directly where you need it.
func main() -> (
[1, 2, 3]: [int32]
| map(x -> x * 2)
)
Inline functions are common with map, filter, sort, group, and related
operations.
Use let for intermediate results¶
let introduces a local name inside an expression.
func main() -> (
let subtotal = 50: int32;
let tax = 10: int32;
subtotal + tax
)
This is helpful when:
- a calculation would otherwise be repeated,
- an expression is becoming hard to read,
- you want to name a meaningful intermediate value.
Return tuples from transformation steps¶
Functions often return tuples so that related values stay together.
func summarize(name: text, score: int32) -> {
name = name,
passed = score >= 50,
score = score,
}
func main() -> summarize("Ada", 88)
This becomes especially important in data work, where each pipeline step often reshapes one tuple into another.
Branch inside a pipeline step¶
Sometimes a transformation depends on a condition.
Use if when you already have a boolean condition and only need two branches.
func label(x: int32) -> if x > 0 then "positive" else "zero or negative"
func main() -> [label(3), label(-1)]
Use match when you want to branch on enum variants.
type Status: enum {
draft,
published,
scheduled: text,
}
func to_text(status: Status) -> match status {
.draft => "draft",
.published => "published",
.scheduled(date) => f"scheduled for {date}",
}
func main() -> to_text(.scheduled("2026-01-01"))
Patterns can also bind values:
type Animal: enum {
cat: text,
dog: text,
}
func greet_animal(animal: Animal) -> match animal {
.cat(name) => f"Hello, {name}!",
.dog(name) => f"Who's a good dog, {name}?",
}
And they can combine cases:
type Kind: enum {cat, dog, hamster}
func needs_walk(kind: Kind) -> match kind {
.cat | .hamster => false,
.dog => true,
}
Use _ when you do not care about the matched value.
Prefer readable pipelines over nested calls¶
This is harder to read:
func main() -> sum(map([1, 2, 3]: [int32], x -> x * 2))
This is usually better:
func main() -> (
[1, 2, 3]: [int32]
| map(x -> x * 2)
| sum()
)
The benefit becomes even clearer with tuple-shaped rows:
const movies = [
{id = 1: int32, title = "Arrival", year = 2016: int32},
{id = 2: int32, title = "Dune", year = 2021: int32},
]
func main() -> (
movies
| map(m -> {
m.id,
title = m.title,
is_recent = m.year >= 2020,
})
)
That left-to-right style is the foundation of tabular-data programming in Lutra.