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 (
!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 (
notin operators (as long as the right-hand side is a list or a range)
- “type-check” functions (
- functions that work on built-in datatypes (
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
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
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.