-
-
Save eksperimental/b767af05913e81184018e2d6977258ec 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. | |
""" | |
@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 __CALLER__.in_guard? 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 __CALLER__.in_guard? 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