Pipes
The pipe operator |> is one of Tova's most ergonomic features. It lets you write data transformation chains that read left-to-right, top-to-bottom, eliminating deeply nested function calls.
Basic Pipe Operator
The |> operator takes the value on the left and passes it as the first argument to the function on the right:
// Without pipes:
nested = upper(trim(" hello "))
// With pipes:
piped = " hello " |> trim() |> upper()Both produce "HELLO", but the piped version reads in the order operations happen: first trim, then uppercase.
How It Works
x |> f()
// is equivalent to:
f(x)
x |> f(a, b)
// is equivalent to:
f(x, a, b)The left-hand value becomes the first argument of the right-hand function call.
Chaining Multiple Operations
Pipes really shine when you chain several transformations:
result = data
|> filter(fn(x) x > 0)
|> map(fn(x) x * 2)
|> sorted()
|> take(5)Compare this to the nested equivalent:
// Nested calls -- reads inside-out
result = take(sorted(map(filter(data, fn(x) x > 0), fn(x) x * 2)), 5)The piped version is far more readable because each step is on its own line and reads in execution order.
Real-World Examples
Processing a list of users:
active_emails = users
|> filter(fn(u) u.active)
|> map(fn(u) u.email)
|> sorted()
|> join(", ")Text processing pipeline:
cleaned = raw_input
|> trim()
|> lower()
|> replace(" ", " ")
|> split(" ")
|> filter(fn(w) len(w) > 0)
|> join(" ")Numerical computation:
average = scores
|> filter(fn(s) s > 0)
|> map(fn(s) s / max_score * 100)
|> sum()
|> fn(total) total / len(scores)Placeholder _
Sometimes you need to pass the piped value as something other than the first argument. Use _ as a placeholder to specify exactly where it goes:
10 |> add(5, _)
// equivalent to: add(5, 10)items |> join(_, ", ")
// equivalent to: join(items, ", ")Placeholder Examples
name = "world"
|> replace(_, "o", "0")
|> fn(s) "Hello, {s}!"
print(name) // Hello, w0rld!// Useful when the function takes the "data" argument second
config |> merge(defaults, _)
// equivalent to: merge(defaults, config)Method Pipe
The method pipe syntax .method() lets you call methods in a pipe chain. The piped value becomes the receiver:
result = " Hello, World! "
|> .trim()
|> .lower()
|> .replace("world", "tova")
// "hello, tova!"This is equivalent to:
result = " Hello, World! ".trim().lower().replace("world", "tova")The method pipe gives you consistent left-to-right reading even when mixing function calls and method calls:
text = raw_input
|> .trim()
|> split(_, ",")
|> map(fn(s) s.trim())
|> filter(fn(s) len(s) > 0)
|> .join("; ")Implicit it Parameter
For simple callbacks in pipe chains, you can use the implicit it parameter instead of writing full lambdas: data |> filter(it > 0) |> map(it * 2). See Functions: Implicit it for details.
Pipes with Lambda Functions
You can pipe into anonymous functions for inline transformations:
result = 42
|> fn(x) x * 2
|> fn(x) x + 1
|> fn(x) "{x} is the answer"
// "85 is the answer"This is occasionally useful for one-off transformations that do not warrant a named function.
Building Pipelines
Pipes encourage a functional, pipeline-oriented style. Here are some common patterns:
Filter-Map-Reduce
total_revenue = orders
|> filter(fn(o) o.status == "completed")
|> map(fn(o) o.total)
|> reduce(fn(sum, t) sum + t, 0)Extract-Transform-Load
fn to_record(row) {
{name: row[0], age: to_int(row[1]), email: row[2]}
}
fn process_csv(raw_csv) {
raw_csv
|> trim()
|> split("\n")
|> map(fn(line) split(line, ","))
|> filter(fn(row) len(row) == 3)
|> map(to_record)
}Validation Chain
fn validate_input(input) {
input
|> trim()
|> fn(s) if len(s) == 0 { Err("Input is empty") } else { Ok(s) }
|> .flatMap(fn(s) if len(s) > 100 { Err("Too long") } else { Ok(s) })
|> .flatMap(fn(s) if s.contains("<") { Err("No HTML allowed") } else { Ok(s) })
}Practical Tips
Use pipes for three or more steps. For a single transformation, a direct function call is fine. Pipes pay off when you chain multiple operations.
One operation per line. Put each pipe step on its own line for readability:
// Good:
readable = data
|> filter(fn(x) x > 0)
|> map(fn(x) x * 2)
|> sum()// Harder to read:
dense = data |> filter(fn(x) x > 0) |> map(fn(x) x * 2) |> sum()Use _ sparingly. If you find yourself using _ frequently, the functions may not be designed for piping. Consider wrapping them in helpers that take the "data" argument first.
Method pipe for fluent APIs. When working with objects that have method chains (like DOM elements or builders), .method() pipe keeps things consistent:
query = builder
|> .select("name", "email")
|> .from("users")
|> .where("active = true")
|> .orderBy("name")
|> .limit(10)