May 09, 2016
Table of contents:
In Elixir, functions are first class citizens. This allows you to define a function and assign it to a variable, and then use that variable to invoke the function.
You can also pass a function as the argument to another function. This is often referred to as anonymous functions or lambdas in other programming languages.
Lambdas that reference a variable from outside of it’s scope are know as Closures.
In today’s tutorial we’re going to be looking at functions as first-class citizens in Elixir, how to assign a function to a variable, and how to pass functions as the arguments to other functions, and how to use Closures in Elixir.
To assign a function to a variable, you would use the following syntax:
double = fn x -> x * 2 end
This defines a function that accepts a single argument. The argument is multiplied by 2 and then returned. An important thing to note is, it is the function definition itself that is bound to the double
variable, not the return value of the function.
When a function bound to a variable, you can then call the function like this:
double.(2)
Anonymous functions are called using dot syntax to differentiate them from regular function calls. When you see a function being called using dot syntax, you will know its an anonymous function, rather than trying to find the regular function definition.
The ability to store a function definition as a variable is really useful. One such use case is the ability to pass a stored function definition to another function.
A lot of different programming languages have this concept of passing a function as an argument to another function.
For example in Ruby, you can pass a function to the map
method on an Object that implements the Enumerable
mixin.
(1..4).map { |i| i * i }
# => [1, 4, 9, 16]
This will iterate over each value and pass it to the given function. The map
method will then return a new array of the return values from the function.
This is the exact same principle that is going on in Elixir. We could rewrite the code above to Elixir like this:
Enum.map(1..4, fn i -> i * i end)
# [1, 4, 9, 16]
As you can see, the Elixir version is pretty much the same as the Ruby version. In this example, I’m passing a Range as the first argument, and an anonymous function as the second argument.
You can think of a Range as just a shorter way of defining a List:
Enum.to_list(1..4)
# [1, 2, 3, 4]
The second argument is the anonymous function, but when you are passing a function to another function as an argument, you don’t need to store it as a variable first.
So as with the Ruby version. The map
function on the Enum
module will iterate through each value of the list and pass it to the function.
The return value from the map
function is a new List containing the return values from the anonymous function. In this case, by multiplying each value by itself.
When reading Elixir code, you will often see the &
operator used in anonymous functions.
For example, the following Elixir code:
Enum.each(1..3, fn x -> IO.puts(x) end)
Can be shortened to:
Enum.each(1..3, &IO.puts/1)
This is known as the capture operator. Under the hood, Elixir is taking the module name, function, and arity and converting it into a lambda for you.
You can also shorten the lambda definition from earlier using the capture operator:
Enum.map(1..4, &(&1 * &1))
Again, Elixir will automatically convert the second argument of the map
function to a lambda.
Inside of the lambda, you can refer to each argument using &n
where n
is the nth argument of the function.
A lambda that references a variable outside of it’s scope is known as a closure.
name = "Philip"
# "Philip"
greet = fn -> IO.puts(name) end
greet.()
# "Philip"
In this example I’ve created a name
variable and bound a value to it.
Next I define a greet
lambda that prints the name
variable to the screen. Notice how the name
variable is not defined inside the scope of the function.
Finally I call the greet
lambda and it prints the name to the screen.
As long as you hold a reference to the greet
lambda, the name
variable will also be available.
For example, you can rebind the name
variable, and then call the greet
lambda again, but the return value won’t change:
name = "Sheila"
# "Sheila"
greet.()
# "Philip"
By holding a reference to the lambda, you will continue to hold a reference to any variables it uses, even if those variables are changed.
Functions as first-class citizens can be confusing at first, especially if you aren’t already familiar with a programming language that has similar concepts.
Fortunately, once you are familiar with the concept, it carries pretty well between languages.
Defining anonymous functions is a really useful thing. For example, passing a lambda function to a function such as map
allows map
to be generic, whilst giving the developer the flexibility to define her own logic.
This is incredibly useful as you will find that you use functions like map
, each
and reduce
a lot in your day-to-day programming!