Elixir : Basics of anonymous functions

Arunmuthuram M
9 min readNov 24, 2023

--

Functions in elixir are first-class citizens that can be bound to a variable, passed as arguments and can be returned from a function just like any other elixir term. Functions in elixir accept data as arguments, transform the data and return a new version of the transformed data. Each function has a distinct functionality and these functions are piped together to perform operations in elixir. Functions in elixir can be of two types such as named functions and anonymous functions. This article is on the basics of anonymous functions in elixir.

Anonymous functions or local functions are what makes elixir treat functions as first class citizens. All three aspects such as being bound to a variable, being passed as an argument and being returned from a function requires a function to be an anonymous function. In order for a named function to do all the above three, it needs to be first converted into an anonymous function. Unlike named functions that can only reside inside a module, anonymous functions can be created both inside modules and outside modules in script files and in iex. They are not tightly bound to a module and can be created and passed around easily just like any other elixir term.

Syntax

An anonymous function can be created using the fn (args) -> body end syntax. It can take any number of parameters. It can be written in a single line, as well as multiple lines with multiple expressions. The parenthesis surrounding the parameters are optional. Unlike named functions, they do not support default arguments.

fn -> "test" end # single line anonymous function with no parameters

fn (a, b) -> a + b end # anonymous function with parameters and parentheses

fn a, b -> a * b end # anonymous function with parameters and no parentheses

fn (num) -> # anonymous function with multi line body
if rem(num, 2) == 0 do
"Even"
else
"Odd"
end
end

Calling an anonymous function

An anonymous function can be called using the function.(args) syntax. Unlike named functions, the parenthesis surrounding the arguments are mandatory when calling anonymous functions, even if it is a no-argument function call. They can also be piped just like named functions.

fn -> "test" end.()
"test"

fn a, b -> a + b end.(1, 2)
3

multiplier = fn (a, b) -> a * b end
adder = fn (a, b) -> a + b end
doubler = fn x -> 2 * x end

multiplier.(5, 5) |> doubler.() |> adder.(10)
60

Capture operator

The capture operator & can be used to convert a named function into an anonymous function and to create anonymous functions using the shorthand syntax. As mentioned before, a named function cannot itself be bound to a variable, passed as an argument or returned from a function. It has to be captured using the & operator as follows. When calling a captured named function, the same number of arguments equal to the named function’s arity must be passed and these arguments will automatically be passed in the same order into the named function.

caps = &String.upcase/1 #bound to a variable
caps.("hello")
"HELLO"

Enum.map(["hello", "world", "test"], &String.upcase/1) #passed as an argument
["HELLO", "WORLD", "TEST"]

Shorthand syntax

The capture operator can be used to create a shorthand version of an anonymous function that has a single expression as its function body. This syntax cannot be used if the function body consists of multiple or block expressions. The syntax involves enclosing the single function body expression within &() and using &1, &2.. etc to access the arguments passed in to the function. &1 corresponds to the first argument passed in and the &2 corresponds to the second argument passed in and so on. Any number of arguments can be accessed this way in the shorthand syntax, but the number of arguments passed in must always be equal to the number of arguments accessed using the &n syntax. The shorthand syntax can only be used if the anonymous function takes in at least a single argument and cannot be used for zero argument functions.

adder = &(&1 + &2)
adder.(1, 2)
3
------------------------------------------------------------------------------

greeter = &("hello") # Fails - shorthand syntax must have at least one argument
error: invalid args for &, expected one of:

* &Mod.fun/arity to capture a remote function, such as &Enum.map/2
* &fun/arity to capture a local or imported function, such as &is_atom/1
* &some_code(&1, ...) containing at least one argument as &1, such as &List.flatten(&1)

Got: "hello"
------------------------------------------------------------------------------

greeter = &("Hello #{&1}.#{&2}")

greeter.("Mr","Satoru")
"Hello Mr.Satoru"

greeter.("Satoru") # Fails - called with one argument instead of two
** (BadArityError) #Function<41.125776118/2 in :erl_eval.expr/6> with arity 2 called with 1 argument ("Satoru")
-------------------------------------------------------------------------------

five_multiplier = &(x = 5
&1 * 5) # Fails - multiple expressions not allowed in shorthand syntax
error: block expressions are not allowed inside the capture operator &, got: x = 5
&1 * 5

&Module.function(&1, &2) syntax

We have seen above that when we are using the capture operator on a named function with the &Module.function/arity syntax, the arguments will automatically be passed into the named function in the same order as they are called. But instead, if you have to pass the arguments manually in a different order, then you can use the syntax &Module.function(&1, &2) .
Let us say that you have a map and you are given a list of keys that you have to delete from the map. Let’s use Enum.reduce to achieve the same. We have to pass the function Map.delete/2 as an argument to the Enum.reduce function.

map = %{1 => :one, 2 => :two, 3 => :three, 4 => :four}
keys = [1,3]
Enum.reduce(keys, map, &Map.delete/2)
** (BadMapError) expected a map, got: 1

The above code will fail. This is because the Map.delete/2 function expects a map as the first argument and the key to delete as the second argument. But when we use the &Map.delete/2 to capture the function, the Enum.reduce will automatically pass the key as the first argument and the accumulator(map) as the second argument, which is not the required order. This can be fixed by wrapping the Map.delete call inside another anonymous function and passing the arguments manually in the required order.

map = %{1 => :one, 2 => :two, 3 => :three, 4 => :four}
keys = [1,3]

Enum.reduce(keys, map, fn (key, acc) -> Map.delete(acc, key) end)
%{2 => :two, 4 => :four}

# with shorthand syntax
Enum.reduce(keys, map, &(Map.delete(&2, &1)))
%{2 => :two, 4 => :four}

To handle the above situation of manually calling arguments on a captured named function, elixir provides an even shorter version of the shorthand syntax without the parenthesis.

map = %{1 => :one, 2 => :two, 3 => :three, 4 => :four}
keys = [1,3]

Enum.reduce(keys, map, &Map.delete(&2, &1))
%{2 => :two, 4 => :four}

Please note that you could still achieve the above using a single Map.drop/2 function call and the Map.delete/2 function is only used for demonstrating the syntax usage.

Shorthand syntax variants

The shorthand anonymous function syntax also provides shorter syntax variants for expressions that return data structures like tuples, lists, maps, strings and binaries. The syntax involves directly using the data structure construct to enclose the expression, eliminating the need for parenthesis. The syntax is &{} for tuples, &[] for lists, &%{} for maps,
&<<>> for binaries and &"" for strings.

list_func = &([&1, &1 * 2, &1 * 3]) # original version with parenthesis
list_func.(1)
[1, 2, 3]

# shorter versions without parentheses
list_func = &[&1, &1 * 2, &1 * 3]
list_func.(1)
[1, 2, 3]

tuple_func = &{&1, &1 * 2, &1 * 3}
tuple_func.(1)
{1, 2, 3}

map_func = &%{&1 => &1 * 2, &1 * 2 => &1}
map_func.(1)
%{1 => 2, 2 => 1}

binary_func = &<<&1, &1 * 2, &1 * 3>>
binary_func.(1)
<<1, 2, 3>>

string_func = &"#{&1} - #{&1 * 2} - #{&1 * 3}"
string_func.(1)
"1 - 2 - 3"

Function clauses in anonymous functions

Anonymous functions support multiple function clauses inside their function body using pattern matching. The syntax starts with the fn keyword followed by multiple patterns of syntax pattern -> function body on each line inside the function body, and is terminated by the end keyword. Similar to named functions, the passed arguments will be matched with the patterns one by one from top to bottom until a pattern matches. Then the matching pattern’s function body is executed. Similar to named functions, function clauses in anonymous functions also support guard clauses.

title_prefixer = fn
title, name when is_binary(name) and name != "" -> title <> name
_, _ -> ""
end

title_prefixer.("Mr.", "Satoru")
"Mr.Satoru"

title_prefixer("Mr.", "")
""

title_prefixer("Mr.", nil)
""

Closures

Anonymous functions in elixir are closures, where the value of visible variables present in the outer scope during the creation of the anonymous functions, are preserved within the anonymous function.

vowel_checker = fn ->
vowels = ["a", "e", "i", "o", "u"]
fn (x) -> x in vowels end
end

func_vowel? = vowel_checker.()

func_vowel?.("a")
true

func_vowel?.("c")
false

In the above example, vowel_checker is a function that returns another function. The returned anonymous function uses the variable vowels defined in its outer scope. Even if vowel_checker goes out of scope, the anonymous function returned from it will preserve, access and use the list bound to vowels, every time the anonymous function is called.

Currying and partial application

Currying is a technique used in functional programming languages where a single function with multiple arguments is broken down into a series of multiple functions with one argument each. Every function in this series returns another function that preserves the previously applied arguments as they are closures. Thus by partially applying one argument to each of the functions in a series, we can manage, compose and reuse functions effectively. When a function with n arguments is broken down into n number of functions with strictly one argument each, then the technique is called currying.

Similarly, when a function with multiple arguments is broken down into multiple functions that have smaller arity but not strictly one, then the technique is called partial application.

Currying and partial application can be performed in elixir using anonymous functions. Let us explain the above concept with an example. Let’s say that you need a function to convert a currency of a particular country into another currency with options like precision. Without currying the function may look like this.

defmodule Currency do
def convert(src_currency, src_currency_val, target_currency, precision) do
rate = get_rate(src_currency, target_currency) # method for getting rate
Float.round(src_currency_val * rate, precision)
end
end

Currency.convert(:usd, 10, :inr, 2)
833.08

Currency.convert(:usd, 15, :inr, 2)
1249.62

As you can see, if the function is used multiple times with the same configuration options, the only argument that changes is the actual source currency’s value. But we still end up passing the same config option arguments every time we need to call the function. Let us modify the above code using partial application.

defmodule Currency do
def converter(src_currency, target_currency, precision) do
rate = get_rate(src_currency, target_currency)
fn (src_currency_val) -> Float.round(src_currency_val * rate, precision) end
end
end

usd_to_inr_converter = Currency.converter(:usd, :inr, 2)

usd_to_inr_converter.(10)
833.08

usd_to_inr_converter.(15)
1249.62

In the above code we use partial application to break down the main function with arity 4 into two functions with arity 3 and 1 respectively. we pass in the 3 config option arguments only once in the first function to get an anonymous function with partially applied arguments. We then invoke the returned function with only the source currency’s value as an argument to get the result. Thus we can pass around this anonymous function, call it only with source currency value and avoid passing in the config options every time.

Even though using the partial application technique gives you better control over composing functions, currying can give you even more control.

defmodule Currency do
def converter(src_currency) do
fn (target_currency) ->
rate = get_rate(src_currency, target_currency)
fn (precision) ->
fn (src_currency_value) ->
Float.round(src_currency_value * rate, precision)
end
end
end
end
end

usd_converter = Currency.converter(:usd)

usd_inr_converter = usd_converter.(:inr)

usd_inr_pres_two_converter = usd_inr_converter.(2)
usd_inr_pres_two_converter.(10)
833.08

usd_inr_pres_three_converter = usd_inr_converter.(3)
usd_inr_pres_three_converter.(10)
833.081

usd_euro_converter = usd_converter.(:eur)

usd_euro_pres_two_converter = usd_euro_converter.(2)
usd_euro_pres_two_converter.(10)
9.17

usd_jpy_pres_two_converter = Currency.converter(:usd).(:jpy).(2)
usd_jpy_pres_two_converter.(10)
1495.9

Currency.converter(:usd).(:sgd).(2).(10)
13.41

As you can see, the main function with arity 4 was curried into a series of 4 functions, each taking exactly one argument. The nested function structure inside the function body ensures that each function returns another function and preserves the previously applied arguments as they are closures. Currying gives you more control by letting you compose functions with respect to each of the config options. Different variations of input config options were easily composed and reused in the above code using currying. The chaining of arguments in the last line of the code is a signature way of calling a curried function.

--

--

No responses yet