haha maybe I think I should break this down more carefully. A macro in elixir is a function that emits AST + “take the AST generated, right here in the code” (a bare function will drop its value, in that location, not the AST). So macros in the first are a function. Let’s consider the “def” macro.
We’ll look at it three ways:
quote do
def foo, do: :bar
end
which yields:
{:def, [context: Elixir, import: Kernel],
[{:foo, [context: Elixir], Elixir}, [do: :bar]]}
and
quote do
def foo do
:bar
end
end
which yields
{:def, [context: Elixir, import: Kernel],
[{:foo, [context: Elixir], Elixir}, [do: :bar]]}
and
quote do
def foo do
:throwaway_statement
:bar
end
end
which yields
{:def, [context: Elixir, import: Kernel],
[
{:foo, [context: Elixir], Elixir},
[do: {:__block__, [], [:throwaway_statement, :bar]}]
]}
Note a) forms 1 and 2 are identical
and b) form 3 groups the two lines of the function into a block.
Why is that necessary? Because according to Kernel.def/2 documentation (https://hexdocs.pm/elixir/Kernel.html#def/2), the function half of the def
macro must take TWO AST parameters. The first parameter is the name of the function (and also a list of arguments) and the second parameter is the contents of the function. So the only arity-consistent way of representing def is by grouping the AST for the body of the function into a block and labeling it as “do”. So the do “keyword” is just elixir’s cute way of saying "hey everything until the “end” keyword winds up in the block. And if you don’t want a block, you can just pass it as do:
, which builds the keyword list manually and passes it into the def function without syntactic sugar.
One more important thing to note is that in elixir, lists, atoms, and two-tuples don’t need escaping, because they are identical to their AST, so that’s how when you do a one-liner the do:
part of the code makes it into the macro without being converted into these strange tuple forms.
as for why it’s a keyword list, if you look further down the documentation, you can have other parts to the function code, for example catch
or rescue
blocks; if you do that, for example this code which is nonsensical:
quote do
def foo do
:bar
catch
:baz
end
end
yields:
{:def, [context: Elixir, import: Kernel],
[{:foo, [context: Elixir], Elixir}, [do: :bar, catch: :baz]]}
so other “segments” of the function can nicely wind up stuffed into an arity-2 statement. Likewise you can arbitrarily mix-and-match catch
es and rescue
s, because a keyword-list is what you are supposed to use when you have a maybe-ordered list of arbitrary things that can’t be a map because you can maybe have duplicates.