Macrofunology 3: guard expressions
The gotcha: in quoted text, guards wrap the head (function name and arglist), so they require special-case code.
Suppose you have a macro like last post's defchain
, a function-defining macro that follows the same structure as def
, with an arglist and do
body. This post is about special code to add if you want to allow the arglist to be annotated with a when ....
guard.
The gotcha: in quoted text, guards wrap the head (function name and arglist), so they require special-case code.
The series: module structure, bodies and arglists, this post, atoms and function names
The current assert_fields
requires a keyword list; that is, a list with tuples containing an atom key and its expected value. Let's extend it so that the list can also contain a lone atom. That means the key can have any value, just so long as it exists:
iex> map = %{a: 3, b: 4}
iex> assert_fields2(map, [:a, :b])
%{a: 3, b: 4}
# A mixture of existence and value checks.
iex> assert_fields2(map, [:a, b: 4])
%{a: 3, b: 4}
The implementation uses pattern matching to distinguish between a tuple and an atom:
The existing defchain
works fine for that: the change is just a do
body to copy verbatim into the quoted text being built. However, let's add a wrinkle. If all a user wants to do is check that a single key exists, it seems rude to require it be wrapped in a list. That is, let's cater to people who prefer the second expression below:
assert_fields(map, [:a])
assert_fields(map, :a )
Using within-function pattern matching would require awkward code. I prefer to write two separate assert_fields
definitions, the first to handle guards, the second to handle the default case:
That compiles fine. If, however, you try to use it, the function will never return. It gets into an infinite loop. What's up with that?
When debugging macros, it's useful to print out their expansion. That's done like this:
quote do defchain assert_fields3...
|> Macro.expand_once(__ENV__)
|> Macro.to_string
|> IO.puts
I typically make a function pe
("print expansion") as shorthand for that.
The expansion of the when is_list
version of assert_fields
looks like this:
def(assert_fields(map, pairs) when is_list(pairs)) do
_called_for_side_effect = for({key, expected} <- pairs) do
assert(Map.get(map, key) == expected)
end
assert_fields(map, pairs) <<<<<<<<<<<<<<<<<
end
Take a look at the highlighted line. Somehow, what should have been a use of map
is a recursive call!
Printing the head
inside of defchain
shows what's going on:
{:when, [], <<<<<<<<<<<<<<<<<<<<<
[
{:assert_fields, [],
[
{:map, [], MacroExamples.ExpansionViewer},
{:pairs, [], MacroExamples.ExpansionViewer}
]},
{:is_list, [context: MacroExamples.ExpansionViewer, import: Kernel],
[{:pairs, [], MacroExamples.ExpansionViewer}]}
]}
Although the when
expression appears after the arglist in the human text, it wraps the arglist in the quoted test. For this case, defchain
's code to extract the first argument:
{_name, _metadata, [return_this | _args]} = head
... actually binds return_this
to {:assert_fields, [], [...args...]}
. So what we thought was an argument name turns out to be a function call.
Therefore, if you want to allow guards, any arglist-processing code needs a case
to handle them:
Next: generating function names.