Macrofunology 4: atoms and function names

Because previous posts looked at function-defining macros that supported the same structure as def, they could ignore the difference between names and atoms. This post will discuss cases where the difference matters.

TL;DR: When creating a function name from an explicit :atom, make sure you provide a parenthesized argument list, even if it's empty:

quote do 
  def unquote(name_atom)() do ...
end

The series: module structure, bodies and arglists, guard expressions, this post


Motivation

Every Erlang/Elixir process has a process dictionary associated with it. Unlike most data structures, it's mutable: editing operations affect the original structure rather than producing a changed copy:

iex(3)> Process.get(:a_key)
nil
iex(4)> Process.put(:a_key, 5)
nil
iex(5)> Process.get(:a_key)
5

We're going to add getter functions for each of a set of valid keys. That is, something like LocalStorage.a_key() would be the way to fetch the value of :a_key. Such getters are sometimes useful for documentation, avoiding bugs due to typos, or simplifying testing.

We'll have two types of getters: one where we assume the process dictionary has a value for the getter's key, and one where a default value is returned if the getter's key is missing. Here's an example of each:

def required_key, do: Process.get(:required_key)
def optional_key, do: Process.get(:optional_key, [])
link to code

(Note that required_key should probably raise an exception if the process dictionary doesn't have the required key. Implementing that uncovers some interesting macro facts I've decided to put in a separate post.)

Getter functions are trivially easy to create: copy an existing one, paste it, and edit. However, I personally am really good at screwing up the trivial. I'm way too prone to copy required_key's definition, change the name of the function, and forget to change the :required_key argument.

So I want a function-defining macro that keeps the function name and dictionary key in sync. Here are two possible examples:

getter  required_key     # name-centric
getter :required_key     # atom-centric

Implementing either macro requires a better understanding of the quoted text representations of names and atoms.

Names and atoms

Here's a macro that prints representations:

    defmacro name_and_atom(name, atom) do
      IO.inspect name, label: "name"
      IO.inspect atom, label: "atom"
    end
link to code

And here's what it prints:

name: {:some_name, [line: 41], nil}
atom: :some_atom

What's printed for a name is a three-tuple like those we earlier saw in a function head. The atom version is different; it's the bare atom itself, not a three-tuple.

Given those representations, we can convert a name to an atom like this:

      {name_atom, _, _} = name

... and an atom to a name like this:

      name = {name_atom, [], nil}

That gives us this name-centric definition for getter:

    defmacro getter(name) do
      {name_atom, _, _} = name
      quote do
        def unquote(name), 
          do: Process.get(unquote(name_atom))
      end
    end
link to code

... and this atom-centric one:

    defmacro getter(name_atom) when is_atom(name_atom) do
      name = {name_atom, [], nil}
      quote do
        def unquote(name), 
          do: Process.get(unquote(name_atom))
      end
    end
link to code

Notice the two have the same quote ... end code.

Defaults

Two syntaxes come to mind for making a getter with a default:

getter optional_key, []
getter optional_key: []

Although I'm hardly an authority, the second seems to me more idiomatic. For one thing, it suggests having a single macro define multiple functions:

getter key1: 1, key2: 2

That's interesting enough that I'll discuss it in its own blog post. For the moment, I'll just implement the single-keyword version. It looks like this:

defmacro getter([{name_atom, default_ast}]) do
  name = {name_atom, [], nil}
  quote do
    def unquote(name), 
      do: Process.get(unquote(name_atom), unquote(default_ast))
  end
end
link to code

(The peculiarity of the argument list shows just how unnatural it is to restrict keyword lists to a single element.)

I want to call attention to the name default_ast. I like to be explicit when a value refers to an abstract syntax tree / quoted text. Why bother? Well, consider this use of getter:

    getter optional_key: :rand.uniform(4) + 5

It's important to remember that default_ast will have this value:

{:+, [line: 68], [
  {{:., [line: 68], [:rand, :uniform]}, [line: 68], [4]},
  5
]}

... and not some constant number between 0 and 9 (which it would if the rand expression were evaluated when the macro was called). When I use names that don't highlight such distinctions, I easily get confused. Most of the time that leads to a compile error, but I worry sometimes it wouldn't.

In addition to intention-revealing names, I think it's a good idea not to test your macros with values that are their own quoted representation. If you use atoms, numbers, strings, or lists of the same, you'll be less likely to discover mistakes. Maps are a good choice:

getter optional_key: %{a: 3}

A map has a satisfyingly complex representation:

iex(10)> quote do: %{a: 3}
{:%{}, [], [a: 3]}

Atoms rule; names drool

Remember that the quoted text for both function names and argument names are three-tuples beginning with an atom:

iex(4)> quote do: assert_fields(map, pairs)
{:assert_fields, [], [
  {:map, [], Elixir}, 
  {:pairs, [], Elixir}]}

The difference is in the third argument. A function name has a list as its third argument.

The Elixir parser knows to create a function-style name when it sees parentheses after the name. If the parentheses are empty, the third argument is an empty list.

However, Elixir goes to some effort to let you leave off parentheses when they're not required. As a result, there's sometimes ambiguity about which kind of name a name-without-parentheses is: function? variable? That's marked by compile warning I bet you've seen:

variable "getter_optional" does not exist and is being expanded to "getter_optional()", please use parentheses to remove the ambiguity or change the variable name

On the Elixir forum, Lud pointed out to me that the same disambiguation rule applies within a macro expansion (quoted text). You can force an atom to be interpreted as a function name by adding parentheses:

iex(7)> quote do: unquote(:atom)
:atom
iex(8)> quote do: unquote(:atom)()
{:atom, [], []}

That gives a more elegant way to create a function's name from an atom: just add parentheses. Like this:

    defmacro getter(name_atom) when is_atom(name_atom) do
      quote do
        def unquote(name_atom)(), 
                              ^^^
          do: Process.get(unquote(name_atom))
      end
    end
link to code

A confession

You'll almost always be using macros to create functions that take at least one argument. I did that for a long time, and I never even realized the difference between names and atoms. I used atoms everywhere because constructs like this:

    quote do
      def(unquote(some_atom)(maplike),
        do: ...
    end

... always worked. It was only when I wrote my first macro without any arguments that I got frustrating failures.

I've belabored the difference between names and atoms to save you from the same sinking feeling that – whereas you thought you understood macros – you suddenly doubt you understand anything.