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:

  defchain assert_fields2(map, key_descriptions) do 
    Enum.each(key_descriptions, fn
      {key, expected} ->
        assert Map.get(map, key) == expected
      key ->
        assert Map.has_key?(map, key)
    end)
  end
link to code

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:

defchain assert_fields3(map, about_keys) when is_list(about_keys) do
                                        ^^^^^^^^^^^^^^^^^^^^^^^
  Enum.each(about_keys, fn
    {key, expected} ->
      assert Map.get(map, key) == expected
    key ->
      assert Map.has_key?(map, key)
  end)
end

defchain assert_fields3(map, one_description) do
  assert_fields3(map, [one_description])
end
link to code

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:

  defp first_arg_name(head) do
    case head do
      {:when, _, [true_head | _]} ->
        first_arg_name(true_head)
      _ -> 
        {_name, _, [first_arg_name | _]} = head
        first_arg_name
    end
  end
link to code

Next: generating function names.