Elixir from the ground up

The pipe: pass this along

The pipe operator takes the value on its left and hands it to the call on its right as the first argument. It changes nothing about the data — only the order in which you read the steps.

Part 7 of 12

Intermediate15 min readBack to Courses

One value, passed along

The pipe is written |>, and it does exactly one thing: it takes the value on the left and gives it to the call on the right as that call's first argument. title |> String.trim() means String.trim(title). Nothing else happens — no hidden state, no mutation, no magic. The value on the left can be any expression, and the call on the right is written without that value; the pipe puts it in place. A chain is therefore a series of calls with one argument already supplied, and every other argument is written where it normally goes.

That makes a chain of steps read like a recipe. Start with the value, then list what happens to it, in the order it happens: trim the text, lower-case it, split it on spaces, join the words with dashes. Written as nested calls the same work has to be read from the inside out, which is exactly backwards from how it runs. That reading order is the whole benefit: a reader can check the first step, then the second, without holding the rest of the nest in their head, and a step that looks wrong is on its own line where it can be pointed at.

Where the pipe fits

Pipes are at their best when a value is being turned into another value in stages, and when every function takes its subject as the first argument. The whole of Enum is built that way, so list |> Enum.filter(f) |> Enum.map(g) |> Enum.join(", ") flows naturally and reads in the same direction as the work. The rule is not "always pipe"; it is "pipe when the subject is the first argument and the chain has more than one step". A single pipe adds a line without adding a step.

Two habits keep pipes honest. Start from a value rather than from a single call — one lone pipe on a line adds nothing. And when a step needs its argument somewhere other than the first position, write it as a plain call; a pipe that hides a reordering is harder to read than the nesting it replaced.

Turning a title into a slug

title = "  Elixir Is Friendly  "

slug =
  title
  |> String.trim()
  |> String.downcase()
  |> String.split(" ")
  |> Enum.join("-")

IO.puts(slug)

# The same two steps, written inside out.
IO.puts(String.length(String.trim(title)))

words = ["pear", "fig", "apple"]
IO.inspect(words |> Enum.sort() |> Enum.join(", "))

A title, four steps, one slug

Run it and count the steps in the chain. Then add a step that replaces spaces with dashes before the join, and write one of the chains without the pipe to see the difference in reading order.

Output
Click "Run" to execute your code

Follow the value, not the function names, when you read a pipe. Each line hands its result to the next one, so the last line of a chain is holding the answer — which is usually the line worth looking at first when the output is not what you expected.

A rewrite, not a runtime step

x |> f(y) is rewritten to f(x, y) before the program runs. Nothing is passed through a channel and nothing is copied; the pipe is punctuation that decides where the value is written. That is why a pipe costs nothing and why it cannot change what the call does. The rewrite is positional: x |> f(y) becomes f(x, y), and x |> f would become f(x), which is why the right side must be a call rather than a bare name. An anonymous function has to be invoked with a dot, so it cannot be the right side of a pipe.

Because the rewrite is positional, the function on the right must take the subject first. Enum and String are written that way on purpose, which is what makes long chains read cleanly. When a function wants its subject second, a pipe would have to reorder the arguments, and a plain call says that more honestly.

Where the pipe stops helping

A pipe is a reading aid, not a rule about how programs should look. When a chain has one step, the plain call is shorter. When a step needs two values from outside, a named function with two arguments says more than a pipe with a reordered call. And when a chain is long enough that you cannot see its start and end on one screen, the middle deserves a name, because a name is a place to stop and check.

A pipeline also hides the shape of its intermediate values. That is fine when each step returns the same kind of thing, and awkward when one step turns a list into a map and the next expects a list. Naming the middle value makes that change visible, and the name is often the only documentation the step needs.

x |> f does not call an anonymous function; write f.(x) instead. A pipe on a single call adds nothing — x |> f() is just f(x) — so start a pipe when the value has several steps to go through. And remember that the value always lands first: y |> f(x) means f(y, x), never f(x, y).

Try these next

  • Turn String.upcase(String.trim(text)) into a two-step pipe.
  • Add a step to a pipe that keeps only the words longer than three letters.
  • Write a four-step pipe and then the same four calls nested, and compare how they read.

Clean a list of names

Run it and follow the five steps from the raw list to the sentence. Then add a name of your own with spaces around it, and add a step that keeps only names longer than three letters.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

What does x |> f(y) mean?