Skip to content

Instantly share code, notes, and snippets.

@christhekeele
Last active September 1, 2020 20:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save christhekeele/8284977 to your computer and use it in GitHub Desktop.
Save christhekeele/8284977 to your computer and use it in GitHub Desktop.
A defguard macro written for Elixir v0.11.something a while back. I don't remember anything breaking at the time. Written for a library that was supposed to help AST transformations, in part by creating guards for particular AST constructs.
defmodule Guard.Helpers do
@moduledoc """
Tools for creating custom guards. Deprecated in favor of `Kernel.defguard`.
"""
@doc """
Creates a macro that's aware of its presence in a guard.
Taken from https://github.com/elixir-lang/elixir/blob/df8b216357e023e4ef078be396fed6b873d6a938/lib/elixir/lib/kernel.ex#L1601-L1615,
good custom guards are written with the following convention:
defmacro is_exception(thing) do
case Macro.Env.in_guard?(__CALLER__) do
true ->
quote do
is_tuple(unquote(thing)) and tuple_size(unquote(thing)) > 1 and
:erlang.element(2, unquote(thing)) == :__exception__
end
false ->
quote do
result = unquote(thing)
is_tuple(result) and tuple_size(result) > 1 and
:erlang.element(2, result) == :__exception__
end
end
end
`Guard.Helpers.defguard` allows you to skip this boilerplate and only write
your logic once, condensing our example above into this:
import Guard.Helpers
defguard is_exception(thing) do
is_tuple(thing) and tuple_size(thing) > 1 and
:erlang.element(2, thing) == :__exception__
end
...which expands to the original code.
Note that this macro does no work to ensure that you only use expressions
allowed in guards. Guard responsibly.
"""
defmacro defguard(guard, [do: code]) do
quote location: :keep, bind_quoted: [
guard: Macro.escape(guard),
code: Macro.escape(code)
] do
case Macro.decompose_call(guard) do
{ name, args } ->
defmacro unquote(name)(unquote_splicing(args)) do
case Macro.Env.in_guard?(__CALLER__) do
true ->
unquote(quotation(code, args, in_guard: true))
false ->
unquote(quotation(code, args, in_guard: false))
end
end
:error ->
:erlang.error ArgumentError.exception(
message: "invalid syntax in defguard #{Macro.to_string(guard)}"
)
end
end
end
def quotation(code, args, in_guard: true) do
{:quote, [], [[do: unquote_vars(code, arg_names(args))]]}
end
def quotation(code, args, in_guard: false) do
{:quote, [], [[do: {:__block__, [],
dequote_args(args) ++ List.wrap(code)
}]]}
end
defp dequote_args(args) do
lc arg inlist args do
{:=, [], [arg, {:unquote, [], [arg]} ]}
end
end
defp arg_names(args) do
Enum.map(args, fn { name, _, _ } -> name end)
end
defp unquote_vars({ token, meta, atom }, arg_names)
when is_atom atom do
case token in arg_names do
true -> { :unquote, [], [{ token, meta, atom }] }
false -> { token, meta, atom }
end
end
defp unquote_vars({ token, meta, args }, arg_names)
when is_list(args) do
{
unquote_vars(token, arg_names),
meta,
Enum.map(args, &unquote_vars(&1, arg_names))
}
end
defp unquote_vars(list, arg_names)
when is_list(list) do
Enum.map(list, &unquote_vars(&1, arg_names))
end
defp unquote_vars({ key, value }, arg_names)
when is_atom(key) do
{ key, unquote_vars(value, arg_names) }
end
defp unquote_vars(quoted_code, _arg_names) do
quoted_code
end
end
@linearregression
Copy link

is this still valid for latest elixir? I don't quite understand the difference between the two cases: one unquote(thing) one by one the other use result to hold unquote(thing)?

@christhekeele
Copy link
Author

christhekeele commented May 20, 2016

The trick to making a macro that works both in and out of guards is only quoting the macro arguments when you're not in a guard––otherwise I think when double unquotes it. This manages that for you is all.

The other trick is keeping to these expressions within the macro: http://elixir-lang.org/getting-started/case-cond-and-if.html#expressions-in-guard-clauses

@eksperimental
Copy link

Hi @christhekeele,
the link to the original is_exception macro in Kernel is outdated. you may want to fix it to
https://github.com/elixir-lang/elixir/blob/df8b216357e023e4ef078be396fed6b873d6a938/lib/elixir/lib/kernel.ex#L1601-L1615

@christhekeele
Copy link
Author

Thanks @eksperimental! Did you take this out for a test drive, does it still happen to work?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment