Last active
September 1, 2020 20:48
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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