A pipe knows which way home is

Nested function calls make the reader enter through the roof and work down towards the front door.

A pipe offers a more ordinary entrance.

Read the work in order

Here are two spellings of the same computation:

import pooka/list

main() {
  let nested = list.reverse(list.map([1, 2, 3], fn(n) { n * 2 }))
  let piped = [1, 2, 3]
    |> list.map(fn(n) { n * 2 })
    |> list.reverse
  assert nested == piped
  assert piped == [6, 4, 2]
}

The pipe supplies the value on its left as the first argument of the call on its right. The explicit callback remains the next argument to list.map.

That convention is why a library’s parameter order matters. Putting the main value first makes an operation easy to use both as an ordinary call and as a step in a pipeline.

The compiler can lower the chain into ordinary operations:

input  = [1, 2, 3]
step1  = list.map(input, double)
result = list.reverse(step1)

This is a lowering sketch, not extra mutable variables in the user’s program. Each intermediate result becomes the input to the next step.

A useful implementation must preserve evaluation order and avoid evaluating the input twice. A compact operator should not quietly duplicate an effectful function call.

Let debugging stay in the stream

Pooka’s echo also works in a pipe:

import pooka/list

main() {
  let result = [1, 2]
    |> list.map(fn(n) { n + 1 })
    |> echo
    |> list.reverse
  assert result == [3, 2]
}

It shows the intermediate value and passes that value onward. The print is explicit; simply returning a value from main should not decide to print it on our behalf.

A pipeline does not make a vague function more informative. The names of its steps still carry the explanation. It does let those names appear in the order their work happens.

Sometimes readability is a very small door, opened in the correct direction.

— Peregrine Finch

· pooka, functions, stdlib