Capturing Functions and Expressions in Elixir

Last reviewed on January 10, 2019

For the past 10 years, my programming experience has mostly focussed on JavaScript and PHP. I've learned a few functional programming concepts through JavaScript, such as function purity, immutability, and side effects, but I've never worked with a true functional language. To learn more about functional programming, I've decided to learn Elixir and write about what I learn to help me learn.

In this post, we're going to look at capturing functions and expressions, which can make Elixir code more concise.

Capturing Functions

In Elixir, we can use Enum.each/2 to loop over an enumerable data type, like a list. If you haven't seen the "/2" notation before, that just denotes the function's arity (number of arguments) since you can define multiple functions with the same name with different arities. With Enum.each/2, the first argument is the enumerable and the second argument is an anonymous function. For example:

Enum.each([1, 2, 3, 4, 5], fn(x) -> IO.puts(x) end)

We can also bind the anonymous function to a variable first:

print_value = fn(x) -> IO.puts(x) end
Enum.each([1, 2, 3, 4, 5], print_value)

This can be a little verbose, especially if we are creating an anonymous function just to call another function, like IO.puts. Why not pass in IO.puts directly? Enum.each/2 expects an anonymous function, and IO.puts/1 is not an anonymous function. It is a named function. However, we can use "&", known as the capture operator, to deal with this:

Enum.each([1, 2, 3, 4, 5], &IO.puts/1)

Here we captured IO.puts/1 by prefixing it with &. This essentially wrapped IO.puts(x) within an anonymous function, like so:

fn(x) -> IO.puts(x) end

Much more terse!

Another way we could have written this is as follows:

Enum.each([1, 2, 3, 4, 5], &IO.puts(&1))

The &1 references the first parameter of the anonymous function that is created when IO.puts/1 is captured. &2, &3, and so on would reference subsequent parameters.

IO.puts(1) is shorthand for IO.puts(:stdio, 1). The above could also be written as such:

Enum.each([1, 2, 3, 4, 5], &IO.puts(:stdio, &1))

You can also write to standard error with:

Enum.each([1, 2, 3, 4, 5], &IO.puts(:stderr, &1))

Capturing Expressions

In addition to capturing functions, we can capture expressions with the &(expression) syntax.

To map a list of numbers to another list where each number is doubled, we can do the following:[1, 2, 3, 4, 5], &(&1 * 2))

To filter a list of numbers to those greater than 3, we can do:

Enum.filter([5, 2, 6], &(&1 > 3))

To sum an array of numbers, we can use Enum.reduce/3. If you aren't familiar with reduce, it is a way to reduce a list to a single value. Summing an array of numbers is one example.

Using an anonymous function in the long form, it looks like the following:

Enum.reduce([1, 2, 3, 4, 5], 0, fn(x, total) -> x + total end)

We can modify this to use a captured expression:

Enum.reduce([1, 2, 3, 4, 5], 0, &(&1 + &2))

Here, &1 represents each number in the list and &2 represents the accumulating total.