Sheharyar Naseer

Writing Composable Guards in Elixir

Elixir ships with a couple of guards like is_map/1, is_binary, etc. which are useful both when writing function clauses as well as in code. But if you find yourself using them in combinations repeatedly for data-type or other checks, a better option would be to define custom guards.

Give me a Guard

Let’s start with a very basic example to go over the syntax. Suppose you often need to validate an age argument in functions and need to make sure it’s a positive integer that’s less than 120. Your functions might end up looking like this:

def update_age(%User{} = user, age) when is_integer(age) and age > 0 and age <= 100 do
  # Return user with new age

With a guard, you could do this instead:

defguard is_valid_age(age) when is_integer(age) and age > 0 and age <= 100

def update_age(%User{} = user, age) when is_valid_age(age) do
  # Return user with new age

Other than defguard, you can also use defguardp. Similar to def and defp, the former is used to define a public guard, which can be called from other modules and the latter is to define a private guard for internal use in a module.

Are you the Struct I’m looking for?

Though elixir already has an is_map struct, I also sometimes needed an is_struct for certain use-cases. So turns out that while Elixir doesn’t have guards to work with maps, Erlang certainly does. We could just use them:

import :erlang, only: [is_map_key: 2, map_get: 2]

defguard is_struct(term) when
  is_map(term) and
  is_map_key(:__struct__, term) and
  is_atom(map_get(:__struct__, term))

defguard is_struct(struct, module) when
  is_struct(struct) and
  map_get(:__struct__, struct) == module

Using them is straight forward:

shey = %Person{name: "Shey", age: 25}

#=> true

is_struct(shey, Person)
#=> true

is_struct(shey, Movie)
#=> false

One real example of using this in the wild is having different user types sharing common functionality/implementations, like Authentication. Imagine a marketplace with completely independent user-types: Supplier, Reseller and Admin - but they share the same authentication logic. You could simplify calls like this:

defmodule Accounts do
  @types [Admin, Supplier, Reseller]

  defguard is_account(account) when
    is_struct(account) and
    map_get(:__struct__, account) in @types

  def authenticate(account, password) when is_account(account) do
    # Common authentication logic

  def delete(account) when is_account(account) do
    # Common deletion logic

  # ...

Joining Them Together

You can continue to write guards and compose them together for a variety of use cases:

defmodule Billing do
  defguard is_subscription(sub) when is_struct(sub, Billing.Subscription)
  defguard is_active(sub) when map_get(:status, sub) == :active
  defguard is_unpaid(sub) when map_get(:status, sub) == :unpaid
  defguard is_trialing(sub) when map_get(:status, sub) == :trialing

  def subscribe(%User{billing: billing}) when is_subscription(billing) do
    {:error, :already_subscribed}

  def subscribe(%User{billing: nil}) do
    # Create and attach a subscription to the user

  def end_trial(%User{billing: billing}) when is_subscription(billing) and is_trialing(billing) do
    # End user's trial

  def end_trial(_user), do: {:error, :not_trialing}

  def cancel(%User{billing: billing}) when is_subscription(billing) and is_unpaid(billing) do
    # Cancel Subscription

Of course the code above could be improved in other ways too, but this was simply meant as a demonstration of guards in elixir. You’ve seen how it can help reduce code repetition, and compared to Macros they can also be used in function clauses. To see more examples, check out the official documentation on HexDocs.