Macros that define functions: conversions between an atom and an AST leaf that is an atom

Sometimes an argument to a macro is to be used as the name of a function being created with def. That’s straightforward[*].

But suppose the name of the function is also to be used as an atom within the body of a function. (Think of using the name to look up a value in the application environment.) Is there a better way to do that than what I show below?


Example 1: config2 from_config2

The config2 macro is to define a zero-argument function from_config2 that returns :from_config2. The following implementation works:

  defmacro config2(quoted_name) do
    # quoted_name is this tuple: `{:from_config_2, [line: 75], nil}`
    name_atom = elem(quoted_name, 0)
    quote do
      def unquote(quoted_name), do: unquote(name_atom)
    end
  end

Plucking a value from a tuple (line 3) seems rather slimy. Is there a better way?

Example 2: config3 :from_config3

This is the same as the above, except that the macro argument is an explicit atom (and thus not wrapped in a tuple). So I can do the wrapping, as in line 3 below:

  defmacro config3(name_atom) do
    # name_atom is just `:from_config_3`
    quoted_name = {name_atom, [], nil}
    quote do
      def unquote(quoted_name), do: unquote(name_atom)
    end
  end

Is there a better way?


[*] If you haven’t done it before, defining a function in a macro looks something like this:

  defmacro defchain(head, do: body) do
    quote do
      def unquote(head) do
        _called_for_side_effect = unquote(body)
        unquote(value_arg(head))
      end
    end
  end

One note: guards make a function definition’s syntax tree a little weird. So if you want to write macros that handle this:

  defchain assert_field(kvs, list) when is_list(list) do
    assert_fields(kvs, list)
  end

… you might have to do some processing like this. That’s the definition of value_arg as used above.

Given that you are looking to define a function call, and to also extract the atom that is the name of the function being called then pattern matching would be more idiomatic but not materially different to using Kernel.elem/2.

Given that that AST:

  • for :fun_name is :fun_name and
  • for fun_name() is {:fun_name, [], [])

Then

{fun_name_as_atom, _meta, _params} = {:fun_name, [], []) would seem to be the idiomatic way to go.

Using your example:

  defmacro config2(quoted_name) do
    # quoted_name is this tuple: `{:from_config_2, [line: 75], nil}`
    {name_atom, _meta, _args} = quoted_name
    quote do
      def unquote(quoted_name), do: unquote(name_atom)
    end
  end
3 Likes

Your examples are fine.

This may be shorter, but may not be better:

  defmacro config2({name, _, []}) do
    quote do
      def unquote(name)(), do: unquote(name)
    end
  end
1 Like

Yes, though I also need to bind a name to the whole tuple:

  defmacro config2({name, _meta, nil} = quoted_name) do

… because in this:

quote do 
  def unquote(_________), do: unquote(________)
end

… the first _______ requires a quoted tuple, whereas the second requires an atom. (If not, it interprets the tuple as a call to an unknown function.)

Thanks.

No because in my snipped I wrote def unquote(name)() (with the parens for the call).

1 Like

Wow! I would not have guessed that would work.

1 Like