Elixir : Basics of named functions

Arunmuthuram M
12 min readNov 22, 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 named functions in elixir.

Named functions

Named functions are functions that are present inside an elixir module. They are associated with a name and an arity. Arity is the number of parameters that a function accepts. Functions names in elixir are snake-cased by convention. When functions are referred from outside the module that they are defined in, a fully qualified name that prefixes the module name and a . symbol before the function name, must be used to access a function. Every named function will be identified by the signature name/arity e.g. String.length/1, Enum.map/2 etc.

Syntax

A named function can be defined using def , followed by the function name, parentheses enclosing the parameters and finally the function body within do and end keywords.

defmodule Test do
def add_num(a, b) do
a + b
end
end

# The named function Test.add_num/2 takes in two arguments and returns
# their sum.

If the function body has only a single expression, then the function can also be defined with a shorter single line syntax without the end keyword.

defmodule Test do
def add_num(a, b), do: a + b
end

The result of the last expression in the function body will be implicitly returned from the function. Named functions can be called using the following syntax. The parenthesis surrounding the arguments is optional when calling named functions.

Test.add_num(1, 2) # arguments surrounded by parenthesis
3

Test.add_num 1, 2 # arguments without parenthesis
3

Some of the other conventions followed when naming a function are, if the function name ends with a ?, then it implies that the function returns a boolean. If the function name ends with a ! , then it implies that this function is capable of raising an error.

Enum.empty?([1,2,3]) # returns boolean
false

Date.new(2023, 11, -24) # returns tuple with error data
{:error, :invalid_date}

Date.new!(2023, 11, -24) # raises a runtime error
** (ArgumentError) cannot build date, reason: :invalid_date

Pipe operator

The pipe operator |> in elixir is used to propagate data from one function to another, improving code readability of expressions with multiple function calls without the need for creating temporary variables to hold intermediate results. The pipe operator lets you create a channel of functions through which the data flows and gets transformed by each function in the pipe. Using the pipe operator picks up the value returned from the previous expression and passes it automatically as the first argument of the next function.

# nested function calls
String.upcase(String.at(String.trim(" hello "), 0))
"H"

# using temporary variables
trimmed = String.trim(" hello ")
first_char = String.at(trimmed, 0)
String.upcase(first_char)
"H"

#using pipe operator
String.trim(" hello ") |> String.at(0) |> String.upcase
"H"

" hello " |> String.trim |> String.at(0) |> String.upcase
"H"

Capturing a named function

In order for a named function to be bound to a variable, passed as an argument or returned from a function, it needs to be converted into an anonymous function using the & capture operator syntax, &function_name/arity. Every time a named function is captured using the &, it will then behave like an anonymous function.

defmodule Test do
def double(a), do: 2 * a
def double_nums(nums), do: Enum.map(nums, &Test.double/1)
end

# In the function body of double_nums, the named function Test.double
# is converted into an anonymous function and sent as
# the second argument to the function Enum.map using the & syntax

Private functions

A private named function is a function that can only be called from within the same module that it is defined in. It will not be accessible outside its own module. These private functions are defined using defp and the rest of the syntax is the same as above.

defmodule Test do
def add(a, b), do: print(a + b)

def multiply(a, b), do: print(a * b)

defp print(result), do: IO.puts("Result - #{result}")
end

Whenever you try to call a function that is either private and out of scope or not present with the same name and arity, then an UndefinedFunctionError is thrown.

Test.invalid(1, 2)
** (UndefinedFunctionError) function Test.invalid/2 is undefined or private

Function clauses

As we have seen above, a named function can be identified using its name and its arity. Function clauses in elixir involve using function definitions of the same name and arity, but with different sets of parameters. Multiple function clauses for a function aids in separating the control flow logic done based on the arguments passed into the function. When there are multiple function clauses with the same signature, elixir implicitly pattern matches each function’s parameters with the arguments and executes the function body of the clause with a successful match. A successful pattern match with a clause’s parameter also binds any parameter variables in the function clause with the respective argument value passed in. Elixir checks the clauses one by one in the same order of definition from top to bottom and executes the first matching clause.

defmodule Test do
def sum(list), do: sum(list, 0)
defp sum(list, acc) do
if list == [] do
acc
else
[hd | tl] = list
acc = acc + hd
sum(tl, acc)
end
end
end

Test.sum([1,2,3])
6

In the above function, the sum of all the numbers of the list is calculated and returned using recursion. Inside the function body, based on the argument being passed, one of the two branches of code is being executed. If the list is empty then the accumulated sum is returned and if not, then the list is pattern matched to get the first element, added to the accumulator and the list’s tail is recursed. The same function can be written using multiple function clauses to remove the argument based control flow inside the function body.

defmodule Test do
def sum(list), do: sum(list, 0)
defp sum([hd | tl], acc), do: sum(tl, acc + hd)
defp sum([], acc), do: acc
end


Test.sum([1,2,3])
6

By having two clauses, one for a non empty list and one for an empty list, the function body will be selected and executed based on the passed in list argument. It is a common convention to group all the function clauses of a function together, in the file that they are defined in.

Pattern matching in function clauses

Now that we know that pattern matching is implicitly used in function clauses, let us see it in detail using the above example.

In the above example, the first function call is Test.sum([1, 2, 3]). Elixir now seeks out the first clause present with signature Test.sum/1. Once it finds the clause, it pattern matches its parameters with the passed in arguments. In our case, the first function clause matching Test.sum/1 is def sum(list), do: sum(list, 0) and so the parameter of this function clause, list will be matched with the passed argument, [1, 2, 3] as
list = [1, 2, 3]. Since the left side is a variable, the term [1, 2, 3] will be bound to the parameter variable list and the match will succeed. Then the function body of the matching clause sum(list, 0) will be executed.

Now, for this function call Test.sum([1, 2, 3], 0), elixir finds the first function clause matching the signature Test.sum/2 which is
defp sum([hd | tl], acc), do: sum(tl, acc + hd). Now, elixir pattern matches the parameters of this function clause with the passed in arguments. In our case [hd | tl], acc = [1 | [2, 3]], 0 matches. Hence the parameter variables hd is bound with 1, tl with [2, 3] and acc with 0 and the function body, sum(tl, acc + hd) is executed as sum([2, 3], 1).

The next function call sum([2, 3], 1) also matches with the parameters of the first clause with the same signature,
defp sum([hd | tl], acc), do: sum(tl, acc + hd) matching parameters and arguments as [hd | tl], acc = [2 | [3]], 1 . Hence the parameter variables are bound to argument values and the function body is executed to call sum([3], 3).

The next function call sum([3], 3) also matches with the parameters of the first clause of defp sum([hd | tl], acc), do: sum(tl, acc + hd).
[hd | tl], acc = [3 | []], 3 matches and hence the parameter variables are bound and the function body is executed to call sum([], 6).

For this function call sum([], 6), the first function clause of signature
defp sum([hd | tl], acc), do: sum(tl, acc + hd) fails to pattern match, since the first argument [] cannot be pattern matched as [hd | tl]. Hence elixir moves on to find the next function clause with the signature Test.sum/2, which is defp sum([], acc), do: acc. The parameters of this clause is pattern matched with arguments to check if there is a match. In our case [], acc = [], 6 is a match and so the variable acc on the left will be bound to the term 6 and the function body is executed to return the value of acc. This is how function clauses implicitly use pattern matching to execute the respective function bodies.

There are certain scenarios where you need to pattern match a collection data structure and extract certain values from it and also use the data structure as a whole. Pattern matching in elixir allows this by both deconstructing and binding the term on the right. This is very common in function clauses with arguments such as lists and maps.

defmodule Test do
def extract_list([first | [second | _] = tl] = list) do
IO.puts(first)
IO.puts(second)
IO.puts(tl)
IO.puts(list)
end

def extract_map(%{a: a_val, b: b_val} = map) do
IO.puts(a_val)
IO.puts(b_val)
IO.inspect(map)
end
end
---------------------------------------------------------------------------
Test.extract_list([1, 2, 3, 4])
1
2
[2, 3, 4]
[1, 2, 3, 4]

Test.extract_map(%{a: 1, b: 2, c: 3, d: 4})
1
2
%{a: 1, b: 2, c: 3, d: 4}

As you can see above, the list and map arguments are both pattern matched to extract specific values and also bound as whole to variables.

FunctionClauseError

Now that we know that elixir pattern matches the passed arguments with parameters of function clauses with the same signature, matching one by one from top to bottom in the same order of definition, what if none of the clauses match the passed arguments ? In this case a FunctionClauseError will be thrown.

defmodule Test do
def sum(list), do: sum(list, 0)
defp sum([hd | tl], acc), do: sum(tl, acc + hd)
defp sum([], acc), do: acc
end

Test.sum({1,2,3})
** (FunctionClauseError) no function clause matching in Test.sum/2

The following arguments were given to Test.sum/2:

# 1
{1, 2, 3}

# 2
0

Default match-all function clause

In order to avoid a FunctionClauseError, a default match-all clause can be used as the last function clause to match all arguments that fail to match the above clauses. They can be used to gracefully handle edge cases and return proper error messages when unexpected arguments are passed in to function.

defmodule Test do
def sum(list), do: sum(list, 0)
defp sum([hd | tl], acc), do: sum(tl, acc + hd)
defp sum([], acc), do: acc
defp sum(_invalid_arg, _acc), do: IO.puts("Error - Argument not a list")
end

Test.sum({1, 2, 3})
Error - Argument not a list
:ok

In the above example, the final match-all clause has two variables as parameters and this ensures that no matter what two arguments are passed in to function, the two parameter variables _invalid_arg and _acc will be bound to the two arguments passed in and the pattern match will always succeed, leading to the execution of the function body that prints the error message.

Underscore wildcard

Notice that, in the above example, the parameter variable names start with an underscore. This is to imply that the variable is not used anywhere in the function body. The values will still be bound to the variables _invalid_arg and _acc when you use the _variable_name syntax. If you name a variable without the underscore prefix and don’t use it in the function body, then a warning will be shown when the code is compiled. Similarly, if you also use a variable with an underscore prefix inside the function body you will also get a warning when the code is compiled.

defmodule Test do
def sum(list), do: sum(list, 0)
defp sum([hd | tl], acc), do: sum(tl, acc + hd)
defp sum([], acc), do: acc
defp sum(_invalid_arg, acc), do: IO.puts("Error -Invalid arg: #{_invalid_arg}")
end

warning: the underscored variable "_invalid_arg" is used after being set.
A leading underscore indicates that the value of the variable should be ignored.
If this is intended please rename the variable to remove the underscore
iex:10: Test.sum/2

warning: variable "acc" is unused (if the variable is not meant to be used,
prefix it with an underscore)
iex:10: Test.sum/2

You could also use just an underscore without any variable name as suffix in these cases. But, this decreases the readability of code. Using _invalid_arg as the variable name instead of just _ makes the code more readable.

defmodule Test do
def sum(list), do: sum(list, 0)
defp sum([hd | tl], acc), do: sum(tl, acc + hd)
defp sum([], acc), do: acc
defp sum(_, _), do: IO.puts("Error - Argument not a list")
end

Test.sum({1, 2, 3})
Error - Argument not a list
:ok

Order of function clauses

We have seen above that the function clauses are matched one by one in the same order of definition. Hence the order in which the function clauses are defined, plays a major role in the execution of clauses. To visualise this, let us reorder the function clauses in the above example and define the default clause before the other two clauses and execute the code.

defmodule Test do
def sum(list), do: sum(list, 0)
defp sum(_invalid_arg, _acc), do: IO.puts("Error - Argument not a list")
defp sum([hd | tl], acc), do: sum(tl, acc + hd)
defp sum([], acc), do: acc
end

Test.sum([1, 2, 3])
Error - Argument not a list
:ok

Test.sum([])
Error - Argument not a list
:ok

Test.sum({1, 2, 3})
Error - Argument not a list
:ok

As you can see, no matter what the passed arguments are, the default clause will match every time since it is the first defined clause. In these cases, you will also get related warnings when the code is compiled.

warning: this clause for sum/2 cannot match because a previous clause at 
line 10 always matches
iex:11

warning: this clause for sum/2 cannot match because a previous clause at
line 10 always matches
iex:12

Default arguments

Named functions in elixir support default arguments using the \\ syntax in the function definition. Any number of default arguments can be provided to the function definition using this syntax.

defmodule Test do
def prefix_title(name, title \\ "Mr."), do: title <> name
end

Test.prefix_title("Satoru")
"Mr.Satoru"

Test.prefix_title("Satoru", "Dr.")
"Dr.Satoru"

Elixir implicitly generates multiple definitions when compiling the code with default arguments. The above code after compiling will have two clause definitions Test.prefix_title/1 and Test.prefix_title/2 as follows.

defmodule Test do
def prefix_title(name), do: prefix_title(name, "Mr.")
def prefix_title(name, title), do: title <> name
end

If the function definition with default arguments has more than one explicit clause, a function header with all the default arguments must be specified before all the other function clauses. Having multiple clauses with default arguments without a function header will lead to a match-all default clause bug where all the function calls will match the compiler generated clause before reaching the other user-defined clauses. In such cases, there will be warnings when the code is compiled.

defmodule Test do
def prefix_title(name, title \\ "Mr."), do: title <> name
def prefix_title("", _title), do: ""
end

warning: def prefix_title/2 has multiple clauses and also declares default values.
In such cases, the default values should be defined in a header. Instead of:

def foo(:first_clause, b \\ :default) do ... end
def foo(:second_clause, b) do ... end

one should write:

def foo(a, b \\ :default)
def foo(:first_clause, b) do ... end
def foo(:second_clause, b) do ... end

iex:20

warning: this clause for prefix_title/2 cannot match because a previous clause
at line 19 always matches
iex:20

The above code will be transformed into the following after the compilation.

defmodule Test do
def prefix_title(name), do: prefix_title(name, "Mr.")
def prefix_title(name, title), do: title <> name
def prefix_title("", _title), do: ""
end

Test.prefix_title("Satoru")
"Mr.Satoru"

Test.prefix_title("")
"Mr."

As you can see, the third clause that handles an empty string argument will never be reached as the compiler generated clause above it will always match any two arguments passed into the function. In order to avoid this, a function header with default arguments and no body should be defined first as follows.

defmodule Test do
def prefix_title(name, title \\ "Mr.") # function header
def prefix_title("", _title), do: ""
def prefix_title(name, title), do: title <> name
end

Test.prefix_title("Satoru")
"Mr.Satoru"

Test.prefix_title("Satoru", "Dr.")
"Dr.Satoru"

Test.prefix_title("")
""

Apart from simple terms, any expression can be bound as a default argument. The expression will be evaluated during run time and will be passed as an argument.

defmodule Test do
def time_to_string(time \\ Time.utc_now), do: Time.to_string(time)
end

Test.time_to_string(~T[11:23:00])
"11:23:00"

Test.time_to_string()
"10:20:18.290000"

Test.time_to_string()
"10:20:19.120000"

This shows that the expression bound as a default argument will be evaluated in the run time and not during the compile time.

Guard clauses

Guard clauses are expressions that return boolean, used along with pattern matching to perform additional assertions on arguments such as type checks. Function clauses in named functions support guard clauses and a pattern is considered a successful match only if the associated guard clauses evaluate to true.

defmodule Test do
def divide(n, x) when is_integer(n) and is_integer(x) and x > 0, do: n/x
def divide(_, _), do: :undefined
end

Test.divide(nil, 4)
:undefined

Test.divide(5, 0)
:undefined

Test.divide(4, 2)
2.0

--

--

No responses yet