Pipes (|>)
a |> f(arg) is exactly f(a, arg). Left-associative; the pipe argument fills the first positional slot of the next call.
Syntax
EXPR "|>" CALLWhere CALL is a function call expression. Chain freely:
let cleaned: str = raw |> str.strip() |> str.lower() |> str.replace(",", "")Desugaring
result = x |> normalise() |> scale(2.0) |> clamp(0.0, 1.0)result = clamp(scale(normalise(x), 2.0), 0.0, 1.0)No partial application, no curry, no special placeholder. Just first-positional threading.
Precedence and positions
A |> chain must end its syntactic slot. Arithmetic (and calls/attribute
access) may feed into the pipe on the left, but no operator may follow the
chain on the same level — a comparison, boolean-op, arithmetic or ternary
tail is a parse error. Parenthesise the chain to use its value further.
A slot is any single expression position: one call argument, a list / set /
tuple element, a dict key or value, a subscript, a keyword-argument or
default value, a lambda body, a comprehension part, a return / assignment
value, an if / elif / while / for … in / assert header, or an
f-string field. The pipe only takes the expression inside its own slot:
1 + 2 |> f() # ✅ == f(1 + 2)g(3 |> f()) # ✅ pipes nest in call argsh(a, b |> f()) # ✅ == h(a, f(b)) — only its own argumentreturn x |> f() # ✅ a bare chain may end a returnif 3 |> f(): # ✅ a chain may be a header(x |> f()) == 0 # ✅ parenthesised: compare the piped value3 |> f() + 1 # ❌ parse error — parenthesise as above3 |> f() == 30 # ❌ parse error — parenthesise as above3 |> f() if cond else 4 |> g() # ❌ parse error — parenthesise each armWhen in doubt, parenthesise.
What’s allowed on the right
The right-hand side must be a call expression. A bare identifier is not enough:
x |> str.upper # ❌ — not a callx |> str.upper() # ✅ — pipe fills the first positional slotMethods accessed on the value itself are allowed, but the pipe still threads into the first positional slot of the resulting call — which is the receiver in method form:
"hello" |> str.upper() # str.upper("hello") → "HELLO""hello".upper() # also "HELLO" — pipes don't replace methodsWhen pipes shine
- Long data-cleaning chains (strings, lists, dataframes).
- Validation pipelines where each step returns the next stage’s input.
- Anywhere the inside-out call form is the reading bottleneck.
For one-step transformations, plain f(x) is fine.
Common patterns
String pipelines
let slug: str = ( title |> str.lower() |> str.strip() |> str.replace(" ", "-"))Multi-line pipe chains must sit inside parentheses — Typhon’s lexer is
otherwise indentation-sensitive on |>, the same way Python is for any
multi-line expression.
List pipelines
let total: int = sum( items |> filter(lambda i: i.active) |> map(lambda i: i.weight))(filter and map return iterators; either pipe into sum() / list() /
tuple() as the terminal step, or wrap the chain in list(...) if you
need a materialised list.)
Numerical pipelines
let smoothed: np.ndarray = signal |> np.convolve(kernel, mode="same") |> np.clip(0, 1)What pipes are not
- Not partial application. There is no
f(_, b)placeholder. The pipe always fills slot 0. - Not curry. No
f(a)(b). - Not type-changing. The output type is whatever the call returns.
- Not async-special. Pipe an async value and you’ll need to
await.
Where next
- Advanced Features (tour) — teaching page.
- Operators and Precedence — full precedence table.