Elixir Function Capturing

The last post covered some bits and bobs about Elixir variable rebinding. This post will aim to cover some interesting things we have learned about defining, and passing around functions in Elixir.

Defining Functions with def

Elixir has multiple ways to define a function. How they are defined can affect how you can invoke them, and how you can pass them around. Let’s start with the classic:

defmodule Announcer do
def announce(announcement) do
IO.puts "I would just like to say #{announcement}"
end
end

This is the first form of defining a function that I came across, it defines a named function with an arity of 1. Arity is just the number of arguments that the function accepts, but it is fundamental to how Elixir treats functions. Interestingly def is a macro, but I won’t dive any further into that in this post. Interestingly named functions must be defined inside a module, trying to define announce outside of a module results in the error:

** (ArgumentError) cannot invoke def/2 outside module
(elixir) lib/kernel.ex:4275: Kernel.assert_module_scope/3
(elixir) lib/kernel.ex:3277: Kernel.define/4
(elixir) expanding macro: Kernel.def/2

That error message might seem quite strange until you remember that def is a macro. Things start to get more interesting when a function is defined with a default argument:

defmodule Announcer do
def announce(announcement \\ "welcome") do
IO.puts "I would just like to say #{announcement}"
end
end

The above definition will, in fact, create an overloaded function with an arity of 0. In other words two functions were defined Announcer.announce/0 and Announcer.announce/1. It is really syntactic sugar for the following:

defmodule Announcer do
def announce() do
announce("welcome")
end
  def announce(announcement) do
IO.puts "I would just like to say #{announcement}"
end
end

One interesting side effect of this is that it doesn’t matter how many default arguments there are, or what order they are in, many other languages enforce that default parameters are at the end of the argument list.

Trying to define an announce/0 after announce/1 with a default parameter results in an error:

iex> defmodule Announcer do
...> def announce(announcement \\ "welcome") do
...> IO.puts "I would just like to say #{announcement}"
...> end
...>
...> def announce() do
...> IO.puts "Override"
...> end
...> end
** (CompileError) iex:21: def announce/0 conflicts with defaults from def announce/1

However, defining announce/0 before defining an announce/1 and a default parameter only results in a warning. Notice how the original definition of announce/0 wins:

iex> defmodule Announcer do
...> def announce() do
...> IO.puts "Not overridden"
...> end
...>
...> def announce(announcement \\ "welcome") do
...> IO.puts "I would just like to say #{announcement}"
...> end
...> end
warning: this clause cannot match because a previous clause at line 18 always matches
iex> Announcer.announce()
Not overriden
:ok

What happens then, when more than one named function is defined with the same arity? Basically the opposite happens, one function gets defined with multiple bodies. So handling the announcement “welcome” differently will result in a single function, with two bodies, that match on the first parameter in order of definition:

defmodule Announcer do
def announce("welcome") do
IO.puts "Welcome everyone"
end
  def announce(announcement) do
IO.puts "I would just like to say #{announcement}"
end
end

As you have seen in this section the announce method can be called with brackets Announcer.announce("announcement"), however, it can also be called without brackets Announcer.announce "announcement". In other words brackets are optional for named functions.

Defining Functions with fn

Elixir supports anonymous (lambda) functions that can be, optionally, bound to variables. Anonymous functions in Elixir are invoked differently to named functions, i.e. myFun.(param), the parameters are mandatory. These functions have the added bonus of being closures:

preamble = "I would just like to say"
announce = fn(announcement) ->
IO.puts "#{preamble} #{announcement}"
end
announce.("hello")

Anonymous functions, declared with fn, support multiple bodies, but must be declared as part of the one anonymous function definition:

announce = fn
announcement
when announcement == "welcome" -> IO.puts "Welcome everyone"
announcement -> IO.puts "I would just like to say #{announcement}"
end

What anonymous functions don’t allow is default parameters because they can only have a single arity. In other words, every body in an anonymous function must have the same arity.

Defining a function with &

Elixir provides a shorthand way to define anonymous (lambda) functions using the & operator:

announce = &(IO.puts "I would just like to say #{&1}")
announce.("hello")

This form of function definition cannot have multiple function bodies, and does not have named parameters, instead it uses parameter place holders &1 to &n.

Capturing named function with &

One thing that we haven’t really touched on so far is how to pass a function as a parameter, accept a function as a parameter, or return a function. Both the fn and & form of defining a function can be passed directly to, or returned from a function. This can be the expression defining the function, or a variable bound to that function definition. It gets a bit more interesting when it comes to passing around named functions. The first thing to remember is that you don’t need parentheses when invoking a named function. So we need a way to pass a named function like Announcer.announce without invoking it with an arity of 0. This is where the capture operator (&) comes into its own. We can capture a named function as follows:

announce = &Announcer.announce/1

The above creates a lambda that calls the announce/1. So it is really just syntactic sugar for:

announce = &(Announcer.announce(&1))

Creating a lambda function that calls the named function enables the named function to be passed to, or returned from a function. A function receiving another function as a parameter can call that function like any other lambda my_fun_param.(). Capturing a named function also has some interesting characteristics in terms of equality:

iex> (&IO.puts/1) == (&IO.puts/1)
true
iex> (&(IO.puts(&1))) == (&(IO.puts(&1)))
true
iex> fn(item) -> IO.puts(item) end == fn(item) -> IO.puts(item) end
false

This is because Elixir does some optimisation under the hood when you are capturing a named function with the arguments in the same order. Another interesting aspect of the capture operator is that it won’t error if a named function doesn’t exist upon capture, only when it is invoked:

iex> unknown = &Unknown.unknown/0
&Unknown.unknown/0
iex> unknown.()
** (UndefinedFunctionError) function Unknown.unknown/0 is undefined (module Unknown is not available)
Unknown.unknown()

Piping to anonymous (lambda) functions

All of the above techniques can of course be used when piping:

"hello"
|> (fn(announcement) -> IO.puts("#{announcement} everyone") end).()

Wrapping Up

Hopefully this has shed some light on some of the some of the various ways to define a function in Elixir, and how that affects how they can be invoked and passed around.