Clarify semantics of defguard, closes #15284
This commit is contained in:
+23
-20
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user