We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
`defguard` creates reusable guard expressions
almirsarajcic
Repeating the same guard logic across multiple functions? Define it once with defguard and use it everywhere - in function heads, case, cond, and with clauses.
defmodule Guards do
@moduledoc "Custom guards for domain-specific validation"
defguard is_positive_integer(value)
when is_integer(value) and value > 0
defguard is_valid_age(age)
when is_integer(age) and age >= 0 and age <= 150
defguard is_valid_email_length(email)
when is_binary(email) and byte_size(email) >= 5 and byte_size(email) <= 254
end
Import and use them exactly like built-in guards:
defmodule Accounts do
import Guards, only: [is_positive_integer: 1, is_valid_age: 1]
def get_user(id) when is_positive_integer(id) do
Repo.get(User, id)
end
def get_user(_), do: {:error, :invalid_id}
def create_user(%{age: age} = attrs) when is_valid_age(age) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
def create_user(_), do: {:error, :invalid_age}
end
Why not just use regular functions?
Guards run at pattern-match time with special optimizations. Regular functions can’t be used in guard position:
# This won't compile - regular functions not allowed in guards
def is_admin?(user), do: user.role == :admin
def admin_action(user) when is_admin?(user) do # CompileError!
# ...
end
# This works - defguard creates a macro that expands in guard context
defguard is_admin(user) when user.role == :admin
def admin_action(user) when is_admin(user) do # Works!
# ...
end
Works everywhere guards work
Custom guards work in all guard contexts:
import Guards, only: [is_positive_integer: 1]
# Function heads
def process(id) when is_positive_integer(id), do: {:ok, id}
# Case expressions
case value do
n when is_positive_integer(n) -> {:positive, n}
n when is_integer(n) -> {:non_positive, n}
_ -> :not_integer
end
# With expressions
with id when is_positive_integer(id) <- Map.get(params, "id") do
get_resource(id)
end
# Anonymous functions
Enum.filter(list, fn x when is_positive_integer(x) -> true; _ -> false end)
Combining guards
Build complex guards from simpler ones:
defmodule Validations do
defguard is_non_empty_string(value)
when is_binary(value) and byte_size(value) > 0
defguard is_uuid_length(value)
when is_binary(value) and byte_size(value) == 36
# Combine existing guards
defguard is_valid_uuid(value)
when is_non_empty_string(value) and is_uuid_length(value)
end
Private guards with defguardp
Use defguardp for module-internal guards:
defmodule OrderProcessor do
# Private - only usable in this module
defguardp is_processable_status(status)
when status in [:pending, :confirmed, :paid]
def process(%Order{status: status} = order) when is_processable_status(status) do
# Process the order
end
def process(%Order{status: status}) do
{:error, {:invalid_status, status}}
end
end
What’s allowed in defguard
Guards must use only allowed expressions - no arbitrary function calls:
Allowed:
- Type checks:
is_integer/1,is_binary/1,is_map/1, etc. - Comparisons:
==,!=,<,>,<=,>=,===,!== - Boolean:
and,or,not - Arithmetic:
+,-,*,/,abs/1,rem/2 - Binary/tuple:
byte_size/1,tuple_size/1,elem/2 - Other guards:
in,is_nil/1,length/1,map_size/1
Not allowed:
- Custom functions
- Side effects
- Exception-raising code
# Won't work - String.contains? isn't a guard-safe function
defguard has_at_sign(email) when String.contains?(email, "@") # CompileError!
# Works - use guard-allowed operations instead
defguard has_minimum_length(str, min) when is_binary(str) and byte_size(str) >= min
defguard has been available since Elixir 1.6. If you’re copy-pasting the same guard expressions across your codebase, extract them into named guards for cleaner, more maintainable code.
Links:
copied to clipboard
Comments (0)
Sign in with GitHub to join the discussion