Notes to self

Elixir macros return AST

Macros are a powerfull part of the Elixir language and projects such as Absinth would not even be possible without them. To start writing your macros in Elixir one has to understand one simple thing. Macro functions have to return a partial abstract syntax tree.

What are macros anyway?

The term macro in this sense comes from the times of assembly languages where a macro defines how to expand a single language statement into a number of instructions. It’s a shortcut on how to write less to achieve more. They also enable expressing something that is not possible in the core language.

In Elixir, many language constructs are in fact macros:

if true, do: "hello world"

In Elixir, if statement above is a macro.

def my_function do
  IO.inspect ""
end

In Elixir, def definition above is a macro as well!

What could be a practical example that might be a little more relevant to us?

Let’s imagine we have the following schema:

defmodule MyProject.Book do
  use Ecto.Schema

  schema "books" do
    field :author, :string
    field :iso, :string
    ...
  end
  ...
end

And an associated Absinth schema:

defmodule MyProject.BookTypes do
  use Absinthe.Schema.Notation

  @desc "Book"
  object :book do
    field :author, :string
    field :iso, :string
    ...
  end
end

First of all, these examples are full of macros already. object is a macro, basically adding a language construct that helps us to define GraphQL objects. So is the field macro.

We see that macros can help us to extend a language or even create a domain-specific language (DSL). But it can just help us with plain and simple repetition as well.

Above, we list all the fields for the Ecto schema and then again for the GraphQL schema. While this gives us flexibility, what if we decide that they are indeed the same and that we just want to maintain one set of attributes?

Completely possible with macros:

defmodule MyProject.BookTypes do
  use Absinthe.Schema.Notation

  @desc "Book"
  object :book do
    magic_macro()
    ...

How? How can magic_macro() fill in what was set of macro calls before?

By returning a part of abstract syntax tree.

Abstract Syntax Tree

An Abstract Syntax Tree (AST) is a tree representation of an abstract structure of a program for its execution flow.

If our macro returns part of the AST it will fit right into the rest of the program execution flow. It’s that simple.

But how do we find out what the AST should look like?

For that, we have to fully understand quoting and unquoting with quote and unquote.

Let’s look at our first example with quote:

iex(1)> quote do
...(1)>   if true, do: "hello world"
...(1)> end
{:if, [context: Elixir, import: Kernel], [true, [do: "hello world"]]}

What we see as the return value is, as you likely guessed, the AST. The building block of an Elixir program. It’s just an Elixir tuple, so nothing to worry about.

Actually… we call it a quoted expression in the Elixir world as the docs puts it:

When quoting more complex expressions, we can see that the code is represented in such tuples, which are often nested inside each other in a structure resembling a tree. Many languages would call such representations an Abstract Syntax Tree (AST). Elixir calls them quoted expressions…

quote is powerful because it will show us any AST representation.

What if we need to pass something in? If our “Hello world” would be a variable?

For that reason, we also get to have unquote:

iex(3)> hello = "Hello world!"
"Hello world!"
iex(4)> quote do
...(4)>   if true, do: unquote(hello)
...(4)> end
{:if, [context: Elixir, import: Kernel], [true, [do: "Hello world!"]]}

Writing a macro

In Elixir, we define a macro similarly as defining a regular function.

However, we use defmacro (which is also a macro!) and we almost always return a quoted expression:

defmacro get_set_value(value) do
  quote do
    def unquote(:"get_#{value}")() do
      # get value...
    end

    def unquote(:"set_#{value}")() do
      # set value...
    end
  end
end

The snipped above demonstrates how to create a macro for defining getter and setters functions automatically for a given value:

...
get_set_value(:attribute)

When we compile our program with this macro, get_set_value would be replaced with those two functions. And their direct AST for that matter.

We even saw a rather strange use of unquote too. Yes, it can be used in defining the function names just fine.

Nice. And what about our Absinth example?

defmodule MyMacros do
  defmacro magic_macro() do
    {:__block__, [],
     Enum.map(MyProject.Book.fields(), fn field_name ->
       {:field, [], [field_name, :string]}
     end)}
  end
end

Since Absinth is built around macros, I first confirmed using quote what Absinth macros return. With that knowledge and given I have a list of the attributes returned by the fields() function, I can replicate this AST in my macro definition.

There is much more to macros, but just remember to return some AST quoted expressions!

Updated: Schrockwell also rightly noted that the plain old functions can also be used to return ASTs at compile-time and that the main difference from macros are in handling of the arguments.

Check out my book
Deployment from Scratch is unique Linux book about web application deployment. Learn how deployment works from the first principles rather than YAML files of a specific tool.
by Josef Strzibny
RSS