Skip to content

Pipes

The pipe operator |> chains transformations left-to-right, making data flow readable.

// Pipe the left side as the first argument to the right side
let result = "hello" |> toUpperCase
// Compiles to: toUpperCase("hello")
let result = users
|> filter(.active)
|> map(.name)
|> sort
|> join(", ")

Compiles to:

let result = join(sort(map(filter(users, (u) => u.active), (u) => u.name)), ", ");

The piped version reads like a recipe: take users, filter, map, sort, join.

When the piped value isn’t the first argument, use _:

let result = 5 |> add(3, _)
// Compiles to: add(3, 5)
let result = value
|> multiply(2)
|> add(10, _)
|> toString

For simple field access or comparisons, use .field:

todos |> Array.filter(.done == false)
todos |> Array.map(.text)
users |> Array.sortBy(.name)

.field creates an implicit closure. .done == false is shorthand for (t) -> t.done == false.

For anything more complex, use an arrow closure:

todos |> Array.map((t) -> Todo(done: !t.done, ..t))

Pipes work with any function, including methods accessed via imports:

import { map, filter, reduce } from "ramda"
let total = orders
|> filter(.status == "complete")
|> map(.amount)
|> reduce((sum, n) -> sum + n, 0, _)

tap runs a side-effect function (like logging) without breaking the pipeline:

let result = users
|> Array.filter(.active)
|> tap(Console.log) // logs filtered users, passes them through
|> Array.map(.name)
|> Array.sort

tap calls the function you give it (for side effects like logging), then returns the original value unchanged. It compiles to an IIFE that calls the function and returns the value.

You can pipe a value directly into match to combine pipelines with pattern matching:

let label = price |> match {
_ when _ < 10 -> "cheap",
_ when _ < 100 -> "moderate",
_ -> "expensive",
}

This is equivalent to match price { ... } but lets you keep the pipeline flowing:

let message = response.status
|> match {
200..299 -> "success",
404 -> "not found",
500..599 -> "server error",
s -> `unexpected: ${s}`,
}

It works at the end of a chain too:

let label = product
|> effectivePrice
|> match {
_ when _ < 10 -> "cheap",
_ when _ < 100 -> "moderate",
_ -> "expensive",
}

x |> match { ... } compiles identically to match x { ... }. It is pure syntax sugar for pipeline ergonomics.

Pipes replace nested function calls with a flat, left-to-right sequence:

Instead ofUse
c(b(a(x)))x |> a |> b |> c
x.map(...).filter(...).reduce(...)x |> map(...) |> filter(...) |> reduce(...)
Temporary variablesDirect piping

Floe has two arrow-like operators:

-> closures, match arms, return types, function types
(x) -> x + 1, Ok(x) -> x, let add(a, b) -> number, (string) -> number
|> pipe data
data |> transform

Each has a distinct purpose. No ambiguity.