Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Guards

guards.livemd

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

Return Home Report An Issue

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"