Mix.install([
{:jason, "~> 1.4"},
{:kino, "~> 0.9", override: true},
{:youtube, github: "brooklinjazz/youtube"},
{:hidden_cell, github: "brooklinjazz/hidden_cell"}
])
Upon completing this lesson, a student should be able to answer the following questions.
- How do you use guards to prevent invalid function input?
- How do you use guards to trigger polymorphic behavior?
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 IntegerGuardExample do
def double(int) when is_integer(int) do
int * 2
end
end
IntegerGuardExample.double(2)
The IntegerGuardExample
module above will only accept integers, so it will crash with a FunctionClauseError if we
provide it a float.
IntegerGuardExample.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 MultipleGuardExample do
def double(value) when is_integer(value) or is_float(value) do
value * 2
end
end
MultipleGuardExample.double(1.3)
While useful for demonstration, we should realistically use the is_number/1
guard instead.
defmodule NumberGuardExample do
def double(value) when is_number(value) do
value * 2
end
end
NumberGuardExample.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)
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 RockPaperScissors do
defguard is_guess(guess) when guess in [:rock, :paper, :scissors]
def winner(guess) when is_guess(guess) do
case guess do
:rock -> :paper
:scissors -> :rock
:paper -> :rock
end
end
end
RockPaperScissors.winner(:rock)
Invalid guesses will now raise a FunctionClauseError.
RockPaperScissors.winner("invalid guess")
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 PolymorphicGuardExample do
def double(num) when is_number(num) do
num * 2
end
def double(string) when is_binary(string) do
string <> string
end
end
PolymorphicGuardExample.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 guard returns true with the provided input will execute.
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 OrderingIssueExample do
def double(num) do
num * 2
end
def double(string) when is_binary(string) do
string <> string
end
end
OrderingIssueExample.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 and our Multiplier.double/1
function works as expected.
defmodule OrderFixedExample do
def double(string) when is_binary(string) do
string <> string
end
def double(num) do
num * 2
end
end
OrderFixedExample.double("example")
OrderFixedExample.double(1)
OrderFixedExample.double(2.5)
Create a Say.hello/1
function which only accepts a string as it's input.
Say.hello("Stephen")
"Hello, Stephen!"
Example Solution
defmodule Say do
def hello(name) when is_bitstring(name) do
"Hello, #{name}!"
end
end
defmodule Say do
def hello(name) do
end
end
Create a Percent.display/1
function which accepts a number 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
Example Solution
defmodule Percent do
def display(percent) when 0 <= percent and percent <= 100 do
"#{percent}%"
end
end
Enter your solution below.
defmodule Percent do
def display(percent) do
end
end
Consider the following resource(s) to deepen your understanding of the topic.
DockYard Academy now recommends you use the latest Release rather than forking or cloning our repository.
Run git status
to ensure there are no undesirable changes.
Then run the following in your command line from the curriculum
folder to commit your progress.
$ git add .
$ git commit -m "finish Guards reading"
$ git push
We're proud to offer our open-source curriculum free of charge for anyone to learn from at their own pace.
We also offer a paid course where you can learn from an instructor alongside a cohort of your peers. We will accept applications for the June-August 2023 cohort soon.