Learn With Me: Elixir - Guard Clauses (#24)
In addition to the pattern matching we learned about previously, you can control which function clause is called using something called guard clauses. A guard clause places further restrictions on the parameters in a function, such as the data type or allowed number range.
In fact, I think that it would be best if you think of guard clauses as a part of pattern matching. I'm pretty sure that Elixir views guard clauses as an integral part of pattern matching, even though they are outside of the function parentheses.
A guard clause begins with the when
keyword and is placed between the function head and the do
keyword. Let's look at a simple example.
def divide(number1, number2) when number2 != 0 do
number1 / number2
end
The guard clause is between the when
and the do
. A call to divide/2
will only pattern match if the number2
parameter is not 0, preventing a divide-by-zero error.
You can find these functions in a module in the code examples in the Learn With Me: Elixir repository on Github so that you can load it into IEx and use it. They are in the "guard_examples.exs" file under the "lwm 24 - Guard Clauses" folder.
iex> GuardExamples.divide(4, 2)
2.0
iex> GuardExamples.divide(1, 2)
0.5
iex> GuardExamples.divide(1, 1)
1.0
iex> GuardExamples.divide(1, -2)
-0.5
iex> GuardExamples.divide(4, 0)
** (FunctionClauseError) no function clause matching in GuardExamples.divide/2
The following arguments were given to GuardExamples.divide/2:
# 1
4
# 2
0
examples/lwm 24/guard_examples.exs:2: GuardExamples.divide/2
iex> GuardExamples.divide(0, 4)
0.0
Attempting to divide by 0 produces an pattern matching error. There are no function clauses that allow a zero in the second parameter.
What's Allowed in a Guard Clause
Guard clauses cannot consist of just anything, but must consist of certain functions and operators. Multiple expressions in guard clauses can be combined with boolean operators.
Only a set of predefined functions in the Elixir library can be used in guard clauses due to the internal workings of the Erlang VM. Other functions cannot be used as guard clauses. Many (but not all) of the functions that can be used in guard clauses start with "is_", a naming convention in Elixir that indicates they are safe to use in guard clauses.
Macros can be used as guard clauses as long as they only contain guard-clause-friendly code. Such macros can be created using the defguard
keyword. I'm not sure what exactly a macro is in the context of Elixir, so this won't make a lot of sense to me until I learn about them. For now, I'm going to assume that they are like macros in C++, which consist of code that is substituted for the macro name in the source code via text replacement just prior to the compilation step.
I haven't found an exhaustive list of exactly which functions are allowed in guard clauses, but this is what the Elixir documentation states:
- comparison operators (
==
,!=
,===
,!==
,>
,>=
,<
,<=
) - strictly boolean operators (
and
,or
,not
). Note&&
,||
, and!
sibling operators are not allowed as they’re not strictly boolean - meaning they don’t require arguments to be booleans - arithmetic unary and binary operators (
+
,-
,+
,-
,*
,/
) in
andnot
in operators (as long as the right-hand side is a list or a range)- “type-check” functions (
is_list/1
,is_number/1
, etc) - functions that work on built-in datatypes (
abs/1
,map_size/1
, etc)
Otherwise, the only real way to know for sure is to try out a function in a guard clause. Elixir will let you know if it's not allowed.
Guard Clause Examples
Let's look at some examples of guard clauses and how we can use them for flow control.
def min(num1, num2) when num1 <= num2 do
num1
end
def min(num1, num2) when num1 > num2 do
num2
end
When num1 <= num2, the first clause is called. Otherwise, the second clause is called.
This allows us to implement a min function without the use of an "if" statement, since the comparison in the guard clause performs that work for us. It's all part of the big pattern matching flow control fun.
iex> GuardExamples.min(-100, 4)
-100
iex> GuardExamples.min(100, 4)
4
iex> GuardExamples.min(100, 404)
100
Here is an example of a function that returns an atom that represents the type of the parameter.
def get_type(data) when is_integer(data) do
:integer
end
def get_type(data) when is_atom(data) do
:atom
end
def get_type(data) when is_function(data) do
:function
end
def get_type(_) do
:other_type
end
The last clause matches any data type not covered by the first three clauses.
iex> GuardExamples.get_type(:ok)
:atom
iex> GuardExamples.get_type(fn x -> x * 2 end)
:function
iex> GuardExamples.get_type(10)
:integer
iex> GuardExamples.get_type("Bob")
:other_type
This example tells us if a list is a "big" list, where we define "big" as having a size of at least 10.
def is_big_list?(list) when is_list(list) and length(list) < 10 do
false
end
def is_big_list?(list) when is_list(list) and length(list) >= 10 do
true
end
The is_list/1
guard clause ensures that the pattern does not match if the parameter is not a list.
iex> GuardExamples.is_big_list?([1, 2, 3])
false
iex> GuardExamples.is_big_list?(Enum.to_list(1..9))
false
iex> GuardExamples.is_big_list?(Enum.to_list(1..10))
true
iex> GuardExamples.is_big_list?(Enum.to_list(1..100))
true
iex> GuardExamples.is_big_list?([])
false
iex> GuardExamples.is_big_list?("Not a list")
** (FunctionClauseError) no function clause matching in GuardExamples.is_big_list?/1
The following arguments were given to GuardExamples.is_big_list?/1:
# 1
"Not a list"
examples/lwm 24/guard_examples.exs:26: GuardExamples.is_big_list?/1
Since anonymous functions can do pattern matching, they can also have guard clauses.
iex> max = fn
...> (num1, num2) when num1 >= num2 -> num1
...> (num1, num2) when num1 < num2 -> num2
...> end
#Function<12.99386804/2 in :erl_eval.expr/5>
iex> max.(1, 1)
1
iex> max.(5, 10)
10
iex> max.(100, -100)
100
iex> max.(0, -2)
0
Programmers from statically-typed languages may be tempted to use guard clauses to try to lock functions down tightly to achieve better type safety. However, Elixir is more Javascript-like (being a dynamically-typed language) in that it's uncommon to do a lot of type checking, whether in guard clauses or not. Elixir code tends not to be overly concerned with type safety, and only checks types where it matters for flow control or to avoid easily-preventable errors when using the function.
Elixir seems to be more relaxed about allowing failure, probably because it has robust ways to recover from failure and unit testing is emphasized. So guard clauses tend to be shorter and Elixir developers don't usually write long guard clauses with lots of type checking. Elixir developers appear to have the attitude of just letting things fail, cleaning up the mess that results, and continuing on.