Clarify semantics of defguard, closes #15284

This commit is contained in:
José Valim
2026-05-13 18:53:13 +02:00
parent 6050b8400a
commit aad63173b8
3 changed files with 71 additions and 22 deletions
+23 -20
View File
@@ -5873,11 +5873,15 @@ defmodule Kernel do
end
@doc """
Defines a macro suitable for use in guard expressions.
Defines a custom guard with the given name.
It raises at compile time if the `guard` uses expressions that aren't
allowed in [guard clauses](patterns-and-guards.html#guards),
and otherwise creates a macro that can be used both inside or outside guards.
Once defined, custom guards can be invoked within regular code or in
guards. The module that contains the custom guard must be required before usage.
Custom guards are defined by providing a valid guard expression to
the right-hand side of `when`. `defguard` will then expand and validate
the expressions as guards. `defguard` will raise at compile time if the
guard uses expressions that aren't allowed in [guard clauses](patterns-and-guards.html#guards).
When defining your own guards, consider the
[naming conventions](naming-conventions.html#is_-prefix-is_foo)
@@ -5885,31 +5889,30 @@ defmodule Kernel do
## Example
For example, to define a guard similar to `Integer.is_even/1`, you can write:
defmodule Integer.Guards do
defguard is_even(value) when is_integer(value) and rem(value, 2) == 0
end
defmodule Collatz do
@moduledoc "Tools for working with the Collatz sequence."
import Integer.Guards
which can then be used as:
@doc "Determines the number of steps `n` takes to reach `1`."
# If this function never converges, please let me know what `n` you used.
def converge(n) when n > 0, do: step(n, 0)
require Integer.Guards
Integer.Guards.is_even(3)
#=> false
defp step(1, step_count) do
step_count
end
## Implementation details
defp step(n, step_count) when is_even(n) do
step(div(n, 2), step_count + 1)
end
Behind the scenes, `defguard` will generate a macro which can be used
inside and outside of guards, preserving their respective semantics.
defp step(n, step_count) do
step(3 * n + 1, step_count + 1)
end
end
When invoked inside a guard, it behaves as if the right-hand side of
`when` is injected as part of the guard, replacing the custom guard
arguments by the expressions given as inputs.
When invoked outside of a guard, it preserves regular function calling
semantics with one caveat: all arguments are evaluated before invocation,
except arguments which are unused, which are then never evaluated.
"""
@doc since: "1.6.0"
@spec defguard(Macro.t()) :: Macro.t()
+10 -2
View File
@@ -339,7 +339,7 @@ defmodule Kernel.Utils do
unquote(literal_quote(unquote_every_ref(expr, vars), []))
false ->
unquote(literal_quote(unquote_refs_once(expr, vars, env.module), generated: true))
unquote(literal_quote(unquote_refs_once(expr, vars, env), generated: true))
end
end
end
@@ -369,7 +369,9 @@ defmodule Kernel.Utils do
end
# Prefaces `guard` with unquoted versions of `refs`.
defp unquote_refs_once(guard, refs, module) do
defp unquote_refs_once(guard, refs, %{module: module} = env) do
env = %{env | context: nil}
{guard, used_refs} =
Macro.postwalk(guard, %{}, fn
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
@@ -391,6 +393,12 @@ defmodule Kernel.Utils do
{var, acc}
end
{{:., dot_meta, [:erlang, :orelse]}, meta, [left, right]}, acc ->
{Macro.expand({{:., dot_meta, [Kernel, :or]}, meta, [left, right]}, env), acc}
{{:., dot_meta, [:erlang, :andalso]}, meta, [left, right]}, acc ->
{Macro.expand({{:., dot_meta, [Kernel, :and]}, meta, [left, right]}, env), acc}
node, acc ->
{node, acc}
end)
@@ -483,6 +483,44 @@ defmodule Kernel.GuardTest do
"""
end
defguard with_or_and_or(foo, bar, baz) when foo or (bar and (baz or foo))
test "expands conditionals" do
args = quote(do: [1 + 1, 2 + 2, 3 + 3])
assert expand_defguard_to_string(:with_or_and_or, args, :guard) == """
:erlang.orelse(1 + 1, :erlang.andalso(2 + 2, :erlang.orelse(3 + 3, 1 + 1)))
"""
assert expand_defguard_to_string(:with_or_and_or, args, nil) == """
{arg1, arg2, arg3} = {1 + 1, 2 + 2, 3 + 3}
case arg1 do
false ->
case arg2 do
false ->
false
true ->
case arg3 do
false -> arg1
true -> true
other -> :erlang.error({:badbool, :or, other})
end
other ->
:erlang.error({:badbool, :and, other})
end
true ->
true
other ->
:erlang.error({:badbool, :or, other})
end
"""
end
defguard in_list(a) when Kernel.in(a, [:test])
test "expands remote functions" do