Pipes

Pipes are a piece of syntax sugar that keep a chain of operations readable. They do not add new behaviour to Par. They just let you write the “do this, then that” story from left to right without inventing throwaway names.

A first taste

Here are some helper functions that operate on numbers:

def Add    = [m: Nat, n: Nat] Nat.Add(m, n)
def Double = [n: Nat] Nat.Add(n, n)
def Square = [n: Nat] Nat.Mul(n, n)

Without pipes you call them inside one another:

let result = Square(Add(3, Double(4)))  // = 196

With pipes you can say the same thing in the order you want to read it:

let result = 3
  -> Add(Double(4))
  -> Square

The pipe feeds the value on the left into the first argument of the function on the right. Nothing else changes — the two version above do the exact same thing.

Expression semantics

Whenever you see

value -> Func(args)

read it as

Func(value, args)

It helps to think of ->Func as one of the operations in the sequence, just like (args) or .case { ... }. You can even rewrite the earlier example as value ->Func (args) to make this explicit.

Account.Lookup(name)
  ->Auth.Check
  .case {
    .ok user   => user.balance
    .err _     => 0
  }

If you need to push the value into a later argument, wrap the call in braces so the whole expression becomes the operation:

value -> {Func(arg1)}(arg3)  // == Func(arg1, value, arg3)

Pipes as commands

In process syntax every command “uses” the variable on its left and stores the result back into that same variable. Pipes follow the same rule, which means you can treat any function as if it were a built-in command.

dec Push : [List<Nat>, Nat] List<Nat>
def Push = [stack, value] .item(value) stack

let stack = *(1, 2, 3)
stack->Push(4)
// exactly the same as: let stack = Push(stack, 4)

Pipes make destructuring commands pleasant too. Take a Pop function that returns a head and a tail:

dec Pop : [List<Nat>] (Option<Nat>) List<Nat>
def Pop = [list] list.case {
  .end! => (.err!) .end!,
  .item(x) xs => (.ok x) xs,
}

let numbers = *(10, 20, 30)
numbers->Pop[top]
// top     : Option<Nat>
// numbers : List<Nat>  -- now *(20, 30)

Again, the command is just a prettier spelling of let (top) numbers = Pop(numbers).

This is the reason the pipe feeds the first argument: it lets you extend the familiar value.operation sequences that already exist in the language without introducing special cases.

Wrapping up

  • Pipes keep left-to-right reading order while preserving Par’s evaluation rules. Drop them and you get the original nested calls back.
  • They play nicely with the rest of the syntax: mix them with case, . selections, further commands — anything that already appends to the sequence.
  • If the first argument is not the one you need, braces give you full control.

Whenever you notice yourself inventing a tmp variable simply to pass it to another function, try a pipe — it’s often all the ceremony you actually need.