Skip to content

Instantly share code, notes, and snippets.

@eksperimental
Forked from christhekeele/guard_helpers.ex
Last active August 13, 2016 13:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eksperimental/b767af05913e81184018e2d6977258ec to your computer and use it in GitHub Desktop.
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.
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