Understanding Elixir’s Macros by Phoenix example

Illustration by Signe Kjær (http://www.signekjaer.dk/)

Macros is very powerful metaprogramming tool. They are used for dynamic code injection. Simply speaking code that writes code.

Macros interact with AST (this will be explained below) and used a lot in the Elixir itself as well as in the Phoenix framework. Elixir is written on Elixir by more than 90%! And macros play a big part in it.

For better understanding why we should care about macros I propose to see how they are used inside the phoenix application.

Let’s start our journey from router because router is something like a gate to our web application.

defmodule Spark.Web.Router do
use Spark.Web, :router
...
end

use Spark.Web, :router will be compiled to next two lines:

require Spark.Web 
Spark.Web.__using__(:router)

Second line will explicitly invoke macro with name __using__ . There is no magic here.

Let’s see what is hiding inside theSpark.Web

defmodule Spark.Web do
...
  defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
end

Here is macro definition: defmacro __using__(which) with one argument and guard clause.

While compilation apply\3 will be transformed to the apply(Spark.Web, :router, [])

Kernel.apply\3 invokes the given function (router) from the module(Spark.Web) with the list of arguments([]).

Let’s proceed with theSpark.Web.router\0 function.

defmodule Spark.Web do
def router do
quote do
use Phoenix.Router
end
end
  ...
  defmacro __using__(which) when is_atom(which) do
apply(__MODULE__, which, [])
end
end

So we can see here that router returns quote construct with use Phoenix.Router inside.

Before digging deeper to the Phoenix itself let’s talk a little bit about quote. This construction (by the way it’s a macro too) transforms block of code to the Abstract Syntax Tree (AST) representation which is just a tuple with 3 elements. On the first place we can see function name, keyword list containing metadata is placed next to it and the third element is the arguments list. Elixir’s AST is the internal representation for functions, operators, etc and it’s the essential part of the metaprogramming and language itself.

Little example:

iex(1)> quote do: search("little bird")
{:search, [], ["little bird"]}
iex(2)> quote do: 3 * 7
{:*, [context: Elixir, import: Kernel], [3, 7]}

In our case use Phoenix.Router will be transformed to this:

{:use, [context: Spark.Web, import: Kernel],
[{:__aliases__, [alias: false], [:Phoenix, :Router]}]}

and invoked inside the macro. Macros always accept and return for the caller quoted expressions.

Let’s go forward and see under the Phoenix’s hood. As we already know use will require Phoenix.Router module and then call __using__ macro.

For those who lost in my explanations I’ve created a simplified version:

defmodule SparkWeb do
defmacro __using__(_) do
quote do
use PhoenixRouter
end
end
end
defmodule PhoenixRouter do
defmacro __using__(_) do
quote do
def test do
IO.inspect __MODULE__
end
end
end
end
defmodule SparkWebRouter do
use SparkWeb
end
SparkWebRouter.test # SparkWebRouter

Did you noticed that inside quote we have caller’s context? It should be stressed that Elixir provides macros hygiene protection. I’m not sure that it’s a good idea to squash everything into the single post, maybe I will write one more about it later.

Hope things became clearer. :)
Let’s return back to the realPhoenix.Router

defmodule Phoenix.Router do
...
  defmacro __using__(_) do
quote do
unquote(prelude())
unquote(defs())
unquote(match_dispatch())
end
end
  ...
end

unquote is used when need to inject values from an outside context (where value was defined).

Simple example:

iex(1)> name = "MoonSpark"
iex(2)> quote, do: "My name is " <> name
{:<>, [context: Elixir, import: Kernel], ["My name is ", {:name, [], Elixir}]}
iex(3)> quote do: "My name is " <> unquote(name)
{:<>, [context: Elixir, import: Kernel], ["My name is ", "MoonSpark"]}

Now take a look please on the prelude() private function:

defmodule Phoenix.Router do
...
  defmacro __using__(_) do
quote do
unquote(prelude())
unquote(defs())
unquote(match_dispatch())
end
end
  defp prelude() do
quote do
Module.register_attribute __MODULE__, :phoenix_routes, accumulate: true
@phoenix_forwards %{}
      import Phoenix.Router
      # TODO v2: No longer automatically import dependencies
import Plug.Conn
import Phoenix.Controller
      # Set up initial scope
@phoenix_pipeline nil
Phoenix.Router.Scope.init(__MODULE__)
@before_compile unquote(__MODULE__)
end
end
...
end

Module.register_attribute __MODULE__, :phoenix_routes, accumulate: true dynamically creates module attribute (something like a constants in other languages)@phoenix_routes for Spark.Web.Router .
You might be wondering why accumulate: true is added? It allows to collect values instead of overriding previous value for the same attribute. New attributes are always added to the top of the accumulated list.

Example:

defmodule Warehouse do
Module.register_attribute __MODULE__, :chest, accumulate: true
  @chest "bow"
@chest "sword"
IO.inspect @chest # => ["sword", "bow"]
end

Next to attribute registration we can see some imports:

import Phoenix.Router
# TODO v2: No longer automatically import dependencies
import Plug.Conn
import Phoenix.Controller

this helps to use functions defined in the Phoenix.Router, Plug.Conn and Phoenix.Controller without namespaces for the macro caller.

One more interesting thing is placed on the last line of this function: @before_compile unquote(__MODULE__)

This attribute helps to notify compiler that before finishing compilation one more step is required. __before_compile__\1 macro should be defined.

We ended just with prelude() , but defs() and match_dispatch() are still not covered by this post. I highly recommend you to open Phoenix’s source code and continue investigating code by yourself. In my opinion it’s one of the fastest ways to become a better elixir developer.

Thank you for reading this post!
See you later!