Understanding Elixir’s Macros by Phoenix example

Mike Andrianov
4 min readJan 28, 2018

--

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

Macro is a 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 are 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 a better understanding of why we should care about macros, I propose to see how they are used inside the phoenix application.

Let’s start our journey from a router because the 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 into the next two lines:

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

The second line will explicitly invoke the macro with the 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 the 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 into the Phoenix itself let’s talk a little bit about quote. This construction (by the way it’s a macro too) transforms a block of code to the Abstract Syntax Tree (AST) representation which is just a tuple with 3 elements. In the first place, we can see the function name, a 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 into this:

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

and invoked inside the macro. Macros always accept and return 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 were 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 notice that inside the quote we have the 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 a single post, maybe I will write one more about it later.

Hope things became clearer. :)
Let’s return 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 attributes (something like constants in other languages)@phoenix_routes for Spark.Web.Router .
You might be wondering why accumulate: true is added? It allows the collection of values instead of overriding previous values 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 the 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 open Phoenix’s source code and continue investigating the 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!

--

--