Skip to content

Commit aad6317

Browse files
committed
Clarify semantics of defguard, closes #15284
1 parent 6050b84 commit aad6317

3 files changed

Lines changed: 71 additions & 22 deletions

File tree

lib/elixir/lib/kernel.ex

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5873,43 +5873,46 @@ defmodule Kernel do
58735873
end
58745874

58755875
@doc """
5876-
Defines a macro suitable for use in guard expressions.
5876+
Defines a custom guard with the given name.
58775877
5878-
It raises at compile time if the `guard` uses expressions that aren't
5879-
allowed in [guard clauses](patterns-and-guards.html#guards),
5880-
and otherwise creates a macro that can be used both inside or outside guards.
5878+
Once defined, custom guards can be invoked within regular code or in
5879+
guards. The module that contains the custom guard must be required before usage.
5880+
5881+
Custom guards are defined by providing a valid guard expression to
5882+
the right-hand side of `when`. `defguard` will then expand and validate
5883+
the expressions as guards. `defguard` will raise at compile time if the
5884+
guard uses expressions that aren't allowed in [guard clauses](patterns-and-guards.html#guards).
58815885
58825886
When defining your own guards, consider the
58835887
[naming conventions](naming-conventions.html#is_-prefix-is_foo)
58845888
around boolean-returning guards.
58855889
58865890
## Example
58875891
5892+
For example, to define a guard similar to `Integer.is_even/1`, you can write:
5893+
58885894
defmodule Integer.Guards do
58895895
defguard is_even(value) when is_integer(value) and rem(value, 2) == 0
58905896
end
58915897
5892-
defmodule Collatz do
5893-
@moduledoc "Tools for working with the Collatz sequence."
5894-
import Integer.Guards
5898+
which can then be used as:
58955899
5896-
@doc "Determines the number of steps `n` takes to reach `1`."
5897-
# If this function never converges, please let me know what `n` you used.
5898-
def converge(n) when n > 0, do: step(n, 0)
5900+
require Integer.Guards
5901+
Integer.Guards.is_even(3)
5902+
#=> false
58995903
5900-
defp step(1, step_count) do
5901-
step_count
5902-
end
5904+
## Implementation details
59035905
5904-
defp step(n, step_count) when is_even(n) do
5905-
step(div(n, 2), step_count + 1)
5906-
end
5906+
Behind the scenes, `defguard` will generate a macro which can be used
5907+
inside and outside of guards, preserving their respective semantics.
59075908
5908-
defp step(n, step_count) do
5909-
step(3 * n + 1, step_count + 1)
5910-
end
5911-
end
5909+
When invoked inside a guard, it behaves as if the right-hand side of
5910+
`when` is injected as part of the guard, replacing the custom guard
5911+
arguments by the expressions given as inputs.
59125912
5913+
When invoked outside of a guard, it preserves regular function calling
5914+
semantics with one caveat: all arguments are evaluated before invocation,
5915+
except arguments which are unused, which are then never evaluated.
59135916
"""
59145917
@doc since: "1.6.0"
59155918
@spec defguard(Macro.t()) :: Macro.t()

lib/elixir/lib/kernel/utils.ex

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ defmodule Kernel.Utils do
339339
unquote(literal_quote(unquote_every_ref(expr, vars), []))
340340

341341
false ->
342-
unquote(literal_quote(unquote_refs_once(expr, vars, env.module), generated: true))
342+
unquote(literal_quote(unquote_refs_once(expr, vars, env), generated: true))
343343
end
344344
end
345345
end
@@ -369,7 +369,9 @@ defmodule Kernel.Utils do
369369
end
370370

371371
# Prefaces `guard` with unquoted versions of `refs`.
372-
defp unquote_refs_once(guard, refs, module) do
372+
defp unquote_refs_once(guard, refs, %{module: module} = env) do
373+
env = %{env | context: nil}
374+
373375
{guard, used_refs} =
374376
Macro.postwalk(guard, %{}, fn
375377
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
@@ -391,6 +393,12 @@ defmodule Kernel.Utils do
391393
{var, acc}
392394
end
393395

396+
{{:., dot_meta, [:erlang, :orelse]}, meta, [left, right]}, acc ->
397+
{Macro.expand({{:., dot_meta, [Kernel, :or]}, meta, [left, right]}, env), acc}
398+
399+
{{:., dot_meta, [:erlang, :andalso]}, meta, [left, right]}, acc ->
400+
{Macro.expand({{:., dot_meta, [Kernel, :and]}, meta, [left, right]}, env), acc}
401+
394402
node, acc ->
395403
{node, acc}
396404
end)

lib/elixir/test/elixir/kernel/guard_test.exs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,44 @@ defmodule Kernel.GuardTest do
483483
"""
484484
end
485485

486+
defguard with_or_and_or(foo, bar, baz) when foo or (bar and (baz or foo))
487+
488+
test "expands conditionals" do
489+
args = quote(do: [1 + 1, 2 + 2, 3 + 3])
490+
491+
assert expand_defguard_to_string(:with_or_and_or, args, :guard) == """
492+
:erlang.orelse(1 + 1, :erlang.andalso(2 + 2, :erlang.orelse(3 + 3, 1 + 1)))
493+
"""
494+
495+
assert expand_defguard_to_string(:with_or_and_or, args, nil) == """
496+
{arg1, arg2, arg3} = {1 + 1, 2 + 2, 3 + 3}
497+
498+
case arg1 do
499+
false ->
500+
case arg2 do
501+
false ->
502+
false
503+
504+
true ->
505+
case arg3 do
506+
false -> arg1
507+
true -> true
508+
other -> :erlang.error({:badbool, :or, other})
509+
end
510+
511+
other ->
512+
:erlang.error({:badbool, :and, other})
513+
end
514+
515+
true ->
516+
true
517+
518+
other ->
519+
:erlang.error({:badbool, :or, other})
520+
end
521+
"""
522+
end
523+
486524
defguard in_list(a) when Kernel.in(a, [:test])
487525

488526
test "expands remote functions" do

0 commit comments

Comments
 (0)