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.
Unfold one link at a time
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