Elixir Pipes

The video below is interesting.

“I really strive to have no local variables in my functions.”

Before:

def total_pieces_on(board) do
  { black, white } = Go.Board.piece_count(board)
  total = black + white
  "Total pieces: #{total}"
end

After:

def total_pieces_on(board) do
  board
  |> Go.Board.piece_count()
  |> (fn { black, white } -> black + white end).()
  |> (&"Total pieces: #{&1}").()
end

I am of two minds about the concept.

  1. You don’t technically need most local variables
  2. …unless you want someone else to understand your code

You have to admit that the first bit of code tells us what’s going on (since the variable names help us to make that determination). It’s not immediately clear in the second version of code above that what is written to the screen is a total of the two; it might take longer to figure this out.

def total_pieces_on(board) do
  { black, white } = Go.Board.piece_count(board)
  "Total pieces: #{black + white}"
end

Presumably this version still works, drops a local variable but keeps just enough in place to inform the next coder what just happened.

1 Like

He took 3 lines of simple code and turned it into 4 lines with a whole bunch of obscure punctuation and strange expressions like “&1”.

No obvious benefit, unless the compiler can figure out a way to make use of multiple threads. He gives no reason why he avoids local variables.

The syntax in Elixir is a bit different than in some other common languages. It gets a lot more readable if you move the anonymous functions into separate functions.

Anonymous functions get called with a dot:

# define an anonymous function
f = fn x, y -> x + y end

# the dot is required
f.(3, 4)
#=> 7

The &1 part is explained here. It’s just capturing the first argument, which is the sum of black + white.

You can make it more readable by moving the anonymous functions into the private section. Then the piped section reads more like English.

defmodule Game do
  @doc """A more readable example."""
  def total_pieces_on(board) do
    board
    |> count_pieces_by_color()
    |> sum_all_pieces()
    |> format_sum()
  end

  # private functions
  defp count_pieces_by_color(_board), do: { 10, 11 } # skipping the computing of pieces
  defp sum_all_pieces({ black, white }), do: black + white
  defp format_sum(total_pieces), do: "Total pieces: #{total_pieces}"
end

Game.total_pieces_on("In a real program, this could be game board data.")
#=> "Total pieces: 21"

His Elixir course is pretty good.

Edit: fixed mistakes in the code example.

It’s possibly a contrived example to explain the concept. This latest version reasonably explains what’s happening by choosing meaningful function names. We assume that the piping mechanism is similar to the dotted notation in JavaScript: (new Date()).setMinutes(0,0,0) in that the output of one is the input of the next.

Not sure what happens if count_pieces_by_color() returns something unexpected to the next function like “hello world!”. Does it break gracefully, is it silently forgiving in a bad way?

Pipes (|>) use the return value of the last function as the first argument of the next function.

So these are the same:

Enum.map([1, 2, 3], fn n -> n * 2 end)
#=> [2, 4, 6]
[1, 2, 3]
|> Enum.map(fn n -> n * 2 end)
#=> [2, 4, 6]

Fault tolerance is one of the main features. This playlist has a good example of a program that has a function that is designed to crash repeatedly, but the program still completes all of its tasks successfully.

It’s worth checking out. :slight_smile:

1 Like