Skip to content

Function chaining

When nested function calls pile up, it gets harder to see the data flow. You can always expand g(f(x)) into separate let bindings, but that adds noise when the intermediate names are not the point.

Function chaining in Sentrie uses the pipeline operator (|>) so the computation reads top-to-bottom: each step takes the value from the line above and passes it into the next call.

let slug = str.replaceAll(str.toLower(str.trim(input)), " ", "-")
let t1 = str.trim(input)
let t2 = str.toLower(t1)
let slug = str.replaceAll(t2, " ", "-")
let slug = input
|> str.trim()
|> str.toLower()
|> str.replaceAll(" ", "-")

The pipeline example above is the same as this nested-call shape (parentheses show the order of application):

let slug = str.replaceAll(
str.toLower(
str.trim(input),
),
" ",
"-",
)

The parser lowers pipeline syntax to ordinary calls:

lhs |> ident() => ident(lhs)
lhs |> alias.fn() => alias.fn(lhs)
lhs |> ident(a, b) => ident(lhs, a, b)
lhs |> alias.fn(a, b) => alias.fn(lhs, a, b)
lhs |> fn(a, #, b) => fn(a, lhs, b)

Pipelines are parser sugar only. They do not introduce new runtime call semantics.

Use # inside a pipeline call target when the piped value should go somewhere other than argument 0.

let replaceChar = "..."
let out = replaceChar |> str.replace(input, #, "$$")

This lowers to:

let out = str.replace(input, replaceChar, "$$")

All # placeholders in the same RHS call bind to the same piped value:

x |> f(#, #)

Lowers to:

f(x, x)
  • Prefer straight chaining for sequential flow: x |> g() |> f().
  • Use # for non-first argument placement when needed.
  • Avoid readability regressions like x |> f(g(#)) when x |> g() |> f() expresses the same logic more clearly.
  • % remains modulo in Sentrie and is unrelated to pipeline placeholders.

The right-hand side of a pipeline must be:

  • A call expression whose callee is an identifier, for example value |> count()
  • A call expression whose callee is a module-qualified field access, for example value |> str.trim()
  • A call expression with explicit arguments, for example value |> str.replaceAll(" ", "-")

The right-hand side is rejected when its callable root is not an identifier or module-qualified field access.

Examples of rejected forms:

value |> (a + b)
value |> foo ? bar : baz
value |> foo[0]
value |> foo().bar

|> has the lowest precedence and associates left-to-right.

value |> str.trim() |> count()

This lowers to:

count(str.trim(value))

For full operator ordering, see Operator Precedence.

Pipeline targets support the same memoization suffixes as ordinary calls:

value |> count()!30
value |> str.trim()!10
value |> str.replaceAll(" ", "-")!60
  • ! enables memoization with default TTL.
  • !<seconds> enables memoization with an explicit TTL in seconds.
  • Supported on call targets (including identifier and module-qualified callees).

value |> trim is rejected at parse time because pipeline RHS targets must be explicit calls (for example trim()).

Name resolution rules remain unchanged.

Pipelines do not inject imported symbols into local scope. use continues to bind module aliases.

use { trim } from @sentrie/string as str
-- Valid: module-qualified access through alias
let a = value |> str.trim()
-- Rejected at parse time (RHS must be an explicit call)
let b = value |> trim