Elixir : Basics of guard clauses

Arunmuthuram M
7 min readNov 26, 2023

--

Guard clauses in elixir are expressions that return boolean, used along with pattern matching to improve their range of assertion capabilities. They aid pattern matching in constructs such as function clauses, case constructs, for comprehension, with, try and receive block etc, enabling type checks and some additional basic assertions on the arguments. Pattern matching eliminates the argument based control flow from the function body and guard clauses enhance this by further eliminating more assertions on arguments from the function body and moving them instead to the pattern match constructs. This article is on the basics of guard clauses, their syntax and usage.

Syntax

Guard clauses start right after the pattern matching construct with the when keyword, followed by a boolean expression containing assertions on the arguments. Multiple boolean expressions can be combined using the strict boolean operators and , or and the not operator can be used to invert the result of an expression. The less stricter versions && , || and ! are not allowed inside the guard clauses. Once the pattern matching succeeds, the assertions of the respective guard clauses are evaluated and the pattern match will be considered a success only if the guard clauses evaluate to true. Otherwise, the pattern match will be considered a failure and the execution will move on to the next pattern.

defmodule Test do # without guards
def print_even_odd(n) do
if is_integer(n) do
if rem(n, 2) == 0, do: IO.puts("Even"), else: IO.puts("Odd")
else
IO.puts("Not an integer")
end
end
end
__________________________________________________________________________________

defmodule Test do # with guards in function clauses
def print_even_odd(n) when is_integer(n) and rem(n, 2) == 0 do
IO.puts("Even")
end
def print_even_odd(n) when is_integer(n), do: IO.puts("Odd")
def print_even_odd(_), do: IO.puts("Not an integer")
end

As you can see in the above code, without guards, the assertions on the argument, is_integer(n) and rem(n, 2) == 0 are present in an if construct inside the function body. But, when we introduce guards, the assertions can be moved outside from the function body, making them much cleaner.

Allowed operators and expressions

Even though guard clauses allow assertions on arguments, the expressions that can be used inside guard clauses are limited by the language to make sure that they are free of side effects and are optimizable for performance and efficiency. The Kernel module contains all of the guard functions and operators that are allowed inside the guard clauses. Let us now see all the allowed operators and expressions inside guard clauses segregated based on data types.

Type check assertions
The various type check based guard functions allowed inside guard clauses are is_atom/1, is_binary/1, is_bitstring/1, is_boolean/1, is_exception/1, is_exception/2, is_float/1, is_function/1, is_function/2, is_integer/1, is_list/1, is_map/1, is_nil/1, is_number/1, is_pid/1, is_port/1, is_reference/1, is_struct/1, is_struct/2 and is_tuple/1

Comparison operators
The comparison operators ==, !=, ===, !==, >, >=, <, <= can be used in guard clauses on arguments of any elixir term. This is because elixir allows comparing values of the same datatype as well as different data types in the following order.

integer < float < atom < reference < function < port < pid < tuple < map
< list < bitstring

Numbers
For number type arguments such as Integer and Float, the arithmetic operators such as +, -, *, / , unary+ and unary- , guard functions such as abs/1, div/2, rem/2, ceil/1, floor/1, round/1 and trunc/1 are allowed in guard clauses. For integers, bitwise operations such as band/2 or &&&, bor/2 or |||, bnot/1 or ~~~, bsl/1 or <<< , bsr/1 or >>> , bxor/2 or ^^^ are also allowed. Please note that the Bitwise module must be imported into the current module before using the above operators in guard clauses.

Boolean
For boolean arguments, as mentioned above, the strict and , or and not operators are allowed inside guard clauses.

Binaries and strings
For String and binary arguments, the binary concatenation operator <> , guard functions such as binary_part/3, bit_size/1 and byte_size/1 are allowed in guard clauses.

Tuples
For tuple arguments, the guard functions elem/2 and tuple_size/1 are allowed inside the guard clauses.

Lists
For list arguments, the guard functions hd/1 , length/1 and tl/1 are allowed inside the guard clauses. The operators in and not in can also be used for lists and ranges in the guard clauses.

Maps
For map arguments, the guard functions map_size/1 and is_map_key/2 can be used in guard clauses.

Errors in guard clauses

When an expression used in guard clauses raises an error, it doesn’t propagate and halt the execution. Instead, it makes the current pattern fail and moves the execution to match the next pattern.

get_val_double = fn 
map, key when not is_map_key(map, key) -> nil
map, key-> map[key] * 2
end

get_val_double.(%{}, 5)
nil

get_val_double.(%{5 => 5}, 5)
10

get_val_double(nil, 5)
** (ArithmeticError) bad argument in arithmetic expression: nil * 2

In the above code, the anonymous function has two function clauses. The first pattern has a guard clause that checks if the first argument, a map, has the second argument, a key, in it. If the key is not present in the map, the guard expression returns true and nil is returned from the function body. When the function is called with a valid map as the first argument, the function works as expected. But when nil is passed as the first argument, it raises an error. This is because the expression used in the first guard clause, is_map_key(nil, 5) leads to an error since the first argument to the expression is nil instead of a valid map. This causes the whole pattern to fail and the execution moves on to the next pattern which matches and the key is directly accessed as nil[5], which in turn returns nil and causes an arithmetic error when nil * 2 is executed. It is essential to use proper assertions like is_map/1 as cases like these would lead to unexpected behaviour during runtime.

One other similar scenario is when using the strict boolean operators and, or and not in the guard clauses, which strictly require either true or false to be operated on. When instead of a strict true or false, they operate on a truthy or falsy value in guard clauses, it will lead to an error and the pattern will be considered a failure, moving the execution to the next pattern.

get_val_double = fn 
map, key when map and is_map_key(map, key) -> map[key] * 2
_, _-> nil
end

get_val_double.(%{5 => 5}, 5)
nil

In the above code, even though a valid map with a valid present key is passed as an argument to the function, nil is returned. This is because the guard clause present in the first pattern contains the strict boolean operator and that operates on a truthy value, map, instead of a strict true or false value. This leads to an error in the guard clause, causing the pattern to fail and moving the execution to the next match-all pattern which succeeds and nil is returned. It is essential to test and verify whether strict boolean values are passed to the strict boolean operators to avoid unexpected behaviour during runtime.

get_val_double = fn 
map, key when map != nil and is_map_key(map, key) -> map[key] * 2
_, _-> nil
end

get_val_double.(%{5 => 5}, 5)
10

Alternate syntax - or operator

When combining multiple expressions with the strict boolean or operator, elixir provides an alternate syntax where each expression can be written in a separate line without using the or keyword for more readability.

large_even_number? = fn n -> # normal syntax with or
case n do
n when n == nil or rem(n, 2) != 0 or n < 100 -> false
_ -> true
end
end

large_even_number? = fn n -> # alternate syntax in case statement
case n do
n when n == nil
when rem(n, 2) != 0
when n < 100 -> false
_ -> true
end
end

large_even_number?.(nil)
false

large_even_number?.(105)
false

large_even_number?.(6)
false

large_even_number?.(126)
true
---------------------------------------------------------------------------
defmodule Test do
def large_even_number?(n) # alternate syntax in function clause
when n == nil
when rem(n, 2) != 0
when n < 100, do: false
def large_even_number?(_), do: true
end

Custom guard clauses

Elixir allows you to create custom guard clauses that can use any of the above allowed expressions and operators. You can combine the allowed expressions and operators into a single custom guard that can be reused instead of rewriting all of the required expressions for the guard clause every time it is used. Private custom guards that are intended to be used only within the same module inside which it has been defined, can be created using the defguardp and public custom guard clauses that can be reused in other modules as well, can be created using defguard. These custom guard clauses will be replaced by the original multiple expressions during compile time. This also requires the custom guard to be available before it is used. Hence if you are using a private guard within the module, the guard must be defined before it is used in the module. If you are using a public custom guard defined in another module, it must be imported using the import keyword, before using it in a guard clause.

The syntax for creating custom guard clauses are defguard or defguardp followed by guard_name(arg1, arg2,..argN) when expression1 and/or expression2.
The naming convention for the custom guards is that the name should be prefixed with a is_ without a ? at the end, so that they can be distinguished from normal functions that return boolean.

defmodule IntegerTest do
defguard is_even(n) when is_integer(n) and rem(n, 2) == 0

def large_even_number?(n) when is_even(n) and n > 100, do: true
def large_even_number?(_), do: false
end
___________________________________________________________________________
defmodule OddTest do
import IntegerTest, only: [is_even: 1]

defguardp is_odd(n) when not(is_even(n))

def large_odd_number?(n) when is_odd(n) and n > 100, do: true
def large_odd_number?(_), do: false
end

--

--

No responses yet