Powered by AppSignal & Oban Pro

Guards

reading/guards.livemd

Guards

Mix.install([
  {:jason, "~> 1.4"},
  {:kino, "~> 0.8.0", override: true},
  {:youtube, github: "brooklinjazz/youtube"},
  {:hidden_cell, github: "brooklinjazz/hidden_cell"}
])

Navigation

Return Home Report An Issue

Setup

Ensure you type the ea keyboard shortcut to evaluate all Elixir cells before starting. Alternatively, you can evaluate the Elixir cells as you read.

Review Questions

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

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 MutlipleGuardExample do
  def double(value) when is_integer(value) or is_float(value) do
    value * 2
  end
end

MutlipleGuardExample.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)

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 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")

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 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)

Your Turn

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

Your Turn

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

Further Reading

Consider the following resource(s) to deepen your understanding of the topic.

Mark As Completed

file_name = Path.basename(Regex.replace(~r/#.+/, __ENV__.file, ""), ".livemd")

save_name =
  case Path.basename(__DIR__) do
    "reading" -> "guards_reading"
    "exercises" -> "guards_exercise"
  end

progress_path = __DIR__ <> "/../progress.json"
existing_progress = File.read!(progress_path) |> Jason.decode!()

default = Map.get(existing_progress, save_name, false)

form =
  Kino.Control.form(
    [
      completed: input = Kino.Input.checkbox("Mark As Completed", default: default)
    ],
    report_changes: true
  )

Task.async(fn ->
  for %{data: %{completed: completed}} <- Kino.Control.stream(form) do
    File.write!(
      progress_path,
      Jason.encode!(Map.put(existing_progress, save_name, completed), pretty: true)
    )
  end
end)

form

Commit Your Progress

Run the following in your command line from the curriculum folder to track and save your progress in a Git commit. Ensure that you do not already have undesired or unrelated changes by running git status or by checking the source control tab in Visual Studio Code.

$ git checkout -b guards-reading
$ git add .
$ git commit -m "finish guards reading"
$ git push origin guards-reading

Create a pull request from your guards-reading branch to your solutions branch. Please do not create a pull request to the DockYard Academy repository as this will spam our PR tracker.

DockYard Academy Students Only:

Notify your teacher by including @BrooklinJazz in your PR description to get feedback. You (or your teacher) may merge your PR into your solutions branch after review.

If you are interested in joining the next academy cohort, sign up here to receive more news when it is available.

Up Next

Previous Next
Metric Conversion with