Guard clauses
A guard clause is a predicate attached to a function definition using the clause, when
. The purpose of guards is to determine if a function should be executed by evaluating a given expression. Elixir executes a function only when the predicate is true. We can think of guards as an extension to pattern matching.
For a function to execute, we first require a match of the parameters. Next, guard clauses determine which function should run. Review the following module:
defmodule Guards do
def double(a) when is_binary(a) do
a <> a
end
def double(a) when is_integer(a) do
a * 2
end
end
The "Guards" module has two functions. Both share the same name and expect one parameter. Yet, the guard clauses make them two separate, distinct functions.
In the first function, the guard clause checks if the argument is a string with the is_binary
predicate. If it is, the two strings are concatenated together with the binary concatenation operator, <>
. If the argument is not a string, the guard clause prevents the first function from running.
The guard clause in the second function checks if the argument passed in is an integer. If it is, the integer is multiplied by 2. When can test out the module, which is saved in a file named "guards.exs", by running iex guards.exs
.
iex(1)> Guards.double(4)
8
iex(2)> Guards.double(16)
32
# when we pass in an integer we get the result of 2x
iex(3)> Guards.double("string")
"stringstring"
iex(4)> Guards.double("rah ")
"rah rah "
# when we pass in a string we get the result of 2x
Valid guard expressions
There are a limited number of expressions that can be used in guard clauses. However, despite being limited, the list is quite extensive. For reference, the following is a comprehensive list of all expressions allowed in guards, taken from the Elixir documentation at HexDocs:
- comparison operators (
==
,!=
,===
,!==
,>
,>=
,<
,<=
) binary concatenation operator (
<>
)in
andnot in
operators (as long as the right-hand side is a list or a range)the following “type-check” functions (all documented in the
Kernel
module):the following guard-friendly functions (all documented in the
Kernel
module):the following handful of Erlang bitwise operations, if imported from the
Bitwise
module:
Custom guards
Custom guards can be created using the existing expressions. This allows us to combine guard expressions for even greater control. Defining custom guards is achieved via defguard
and defguardp
. The latter defines a private guard.
For example, supposed you want to ensure an argument is not only an integer but also an even number. Remember, the rem
function returns the remainder of two dividends. Therefore:
defmodule Guards do
# using the defguard construct and naming the guard, "is_even"
defguard is_even(value) when is_integer(value) and rem(value, 2) == 0
... # is_binary function
# let's say we want to square even numbers instead of doubling them
# we MUST place the is_even guard above the is_integer guard
# otherwise is_integer would match before our is_even clause
def double(a) when is_even(a) do
a * a
end
def double(a) when is_integer(a) do
a * 2
end
end
We create our custom guard, "is_even", placing it above the "integer" guard. Elixir executes files from top to bottom. Therefore, it's important to recognize the order of our functions. With the setup we have above, we can reload the current iex session by typing r Guards
.
iex(5)> r Guards
{:reloaded, Guards, [Guards]}
iex(6)> Guards.double(9)
18
iex(7)> Guards.double(10)
100
# our custom guard worked! the even number was squared
Line 7 shows that our custom guard worked. Now, let's examine how to further take advantage of Elixir's sequential processing. We know how strings, even numbers, and odd numbers will be treated in our "Guards" module. However, what if someone passes in an argument that isn't a binary string or integer? Elixir would search the file from top to bottom without any luck in finding how to handle the input. Therefore, let's define a function that will execute only if the argument received is not a string or integer.
defmodule Guards do
# ...
# functions removed for brevity
def double(a) do
IO.puts("you didn't pass an int or string!!")
end
end
Here we declare a function at the bottom of the file. It's purpose is to catch every instance an argument is not an integer or string.
# reload the module
iex(8)> r Guards
{:reloaded, Guards, [Guards]}
# pass in something other than int or string
iex(9)> Guards.double([1,2,3]}
you didn't pass an int or string!!
:ok
# pass in something other than int or string
iex(10)> Guards.double(:atomic)
you didn't pass an int or string!!
:ok
The purpose of this example is to demonstrate that we don't have to account for every possible data type in our function definition. By defining the types we want to handle using guards, we can define a single catch-all function at the bottom of our module to handle the alternative cases.