Guards
Mix.install([
{:kino, github: "livebook-dev/kino", override: true},
{:kino_lab, "~> 0.1.0-dev", github: "jonatanklosko/kino_lab"},
{:vega_lite, "~> 0.1.4"},
{:kino_vega_lite, "~> 0.1.1"},
{:benchee, "~> 0.1"},
{:ecto, "~> 3.7"},
{:math, "~> 0.7.0"},
{:faker, "~> 0.17.0"},
{:utils, path: "#{__DIR__}/../utils"}
])
Navigation
Guards
Guards allow you to guard your functions to only accept certain input.
flowchart LR
Input --> Guard --> Function
Guard --> f[Invalid Input] --> fc[FunctionClauseError]
This prevents a function from being misused and provides clearer feedback.
Guards start with a when
keyword followed by a boolean expression. For example,
defmodule Multiplier do
def double(int) when is_integer(int) do
int * 2
end
end
Multiplier.double(2)
The Multiplier
module above will only accept integers, so it will crash with a FunctionClauseError
if we
provide it a float.
Multiplier.double(1.5)
You can use multiple guards together in the same function head. For example, we could
use both is_integer/1
and is_float/1
in the same double/1
function.
defmodule Multiplier do
def double(value) when is_integer(value) or is_float(value) do
value * 2
end
end
Multiplier.double(1.3)
While useful for demonstration, we should realistically use the is_number/1
guard instead.
defmodule Multiplier do
def double(value) when is_number(value) do
value * 2
end
end
Multiplier.double(1.3)
Guards must return true
or false
values and be pure input and output.
You can reference the Guard Documentation for a full list.
-
Comparison Operators (
==
,!=
,===
,!==
,<
,<=
,>
,>=
) -
Boolean Operators (
and
,or
,not
). -
Arithmetic Operators (
+
,-
,*
,/
) -
Membership Operator
in
. -
“type-check” functions (
is_list/1
,is_number/1
,is_map/1
,is_binary/1
,is_integer/1
etc) -
functions that work on built-in datatypes (
hd/1
,tail/1
,length/1
and others)
defguard
You can define your own custom guard with defguard
. For example, let’s say
we’re building a rock paper scissors game which should only accept :rock
, :paper
, and :scissors
as
valid guesses.
We could create a is_guess/1
guard which checks that the guess is valid.
defmodule RPS do
defguard is_guess(guess) when guess in [:rock, :paper, :scissors]
def play(guess) when is_guess(guess) do
case guess do
:rock -> :paper
:scissors -> :rock
:paper -> :rock
end
end
end
RPS.play(:rock)
Invalid guesses will now raise a FunctionClauseError
.
RPS.play("rock")
Polymorphic Functions With Guards
You can also use guards in combination with multi-clause functions to achieve polymorphism.
For example, let’s say we want the double
function to handle strings.
So double
called with "hello"
would return "hellohello"
.
flowchart LR
2 --> a[double] --> 4
hello --> b[double] --> hellohello
We can use the built-in is_binary
guard to check if the variable is a string.
That’s because internally strings in Elixir are represented as binaries.
defmodule Multiplier do
def double(num) when is_number(num) do
num * 2
end
def double(string) when is_binary(string) do
string <> string
end
end
Multiplier.double("example")
There are many guards available in Elixir. If you ever need a specific guard, you can refer to the Guards documentation.
Function order matters. The first function who’s function head matches the input executes.
For example. if you remove
the is_number/1
guard. Now the first function expects any type of input, so it will always
execute instead of the is_binary/1
version.
defmodule Multiplier do
def double(num) do
num * 2
end
def double(string) when is_binary(string) do
string <> string
end
end
Multiplier.double("example")
You’ll notice our program crashes with an error. Elixir also provides a handy warning to let us know that the first function clause always matches so the second will never execute.
> this clause for double/1 cannot match because a previous clause at line 2 always matches.
If you move the more generic function lower, then strings will match the
is_binary/1
version first.
defmodule Multiplier do
def double(string) when is_binary(string) do
string <> string
end
def double(num) do
num * 2
end
end
Multiplier.double("example")
Multiplier.double(1)
Multiplier.double(2.5)
Your Turn
Create a Say.hello/1
function which only accepts a string as it’s input.
Say.hello("Brian")
"Hello, Brian."
defmodule Say do
def hello(name) do
end
end
Utils.feedback(:say_guards, Say)
Create a Percent.display/1
function which accepts an integer and returns a string with a percent.
Use guards to ensure the percent is between 0 (exclusive) and 100 (inclusive)
Percent.display(0.1)
"0.1%"
Percent.display(100)
"100%"
Percent.display(0)
** (FunctionClauseError) no function clause matching in Percent.display/1
Percent.display(101)
** (FunctionClauseError) no function clause matching in Percent.display/1
defmodule Percent do
def display(percent) do
end
end
Utils.feedback(:percent, Percent)
Commit Your Progress
Run the following in your command line from the project folder to track and save your progress in a Git commit.
$ git add .
$ git commit -m "finish guards section"