Quick introduction to macro in Elixir

Back

When i first ran into macro in Elixir, i felt it was too much for me to learn on the first round. The concept seemed too stepped to digest for the moment. Immutability, pattern matching and Erlang processes are already pretty difficult to grasp when you come from utterly different environment (Java & Javascript in my case).

What a regret i have now! Even if macros are a powerful and complex tool, you can use them to make code much more readable without having to enter the hard bits. That's what i'll try to prove you with some simple examples.

What is a macro

Elixir is a compiled language based on Erlang. Compilation time can be annoying, but it provides more powerful tools such as code generation through their preprocessors. This feature is called a macro in Elixir. When I learned C I felt this was a useless feat, but now that I've grown aware to readability and reusability in programming, thanks to Software Craftsmanship mentality, I see macro a very different way.

Macro allow you to use Elixir code to generate code, which can help you design complex APIs and sleek domain specific languages. But with great power comes great responsabilities, and there could be some issues:

  • It hides information that developer might want to access to
  • It is not as easy to debug as plain code
  • Complex macro can inflate compilation time by a lot

We won't cover those as they are specific issues that could be very dependent on the context or subjective perception.

Our first macro

Let's say I want to spell numbers literally, write two instead of 2 for instance. Nothing more simple:

defmodule Integers.Macro do
  defmacro two, do: 2
end
defmodule Integers do
  import Integers.Macro
  
  def addTwo(addend), do: two + addend
end

This example is straight forward, the final result is exactly the same as if we'd use a function. The only difference is that the expression two is not evaluated at runtime but at compilation.

The process of generating code is call macro expand, and the above code generates the very expected:

def addTwo(addend), do: 2 + addend

So far so good, as the expanded version is arguably more readable then its macro version. Let's dig in a more useful example.

A more realistic use case

When i started this blog, I wanted to sanitize the content of my articles. Remove traces of classes, and eventually protect users from my mistakes by removing any hazardous inline scripts. The excellent html_sanitize_ex provides such functionality.

Unfortunately, the basic sanitizer was sanitizing more than i would like. I will cut down the part of me reading the code, it's pretty straight forward (like any Elixir code base to be honest). I ended up on the module which scrubs every unwanted html tags. It has a very elegant usage of macro. The scubber looks like:

# ...
Meta.allow_tag_with_these_attributes "hr", []
Meta.allow_tag_with_these_attributes "i", []

Meta.allow_tag_with_uri_attributes   "img", ["src"], ["http", "https"]
Meta.allow_tag_with_these_attributes "img", ["width", "height", "title", "alt"]

Meta.allow_tag_with_these_attributes "li", []
Meta.allow_tag_with_these_attributes "ol", []
Meta.allow_tag_with_these_attributes "p", []
# ...

What is really nice with this way of writing is that you understand almost immediately what the code is doing. If i were to ask you to add the iframe to the list of allowed tag, you would probably come up with:

Meta.allow_tag_with_uri_attributes "iframe", ["src"], ["http", "https"]

Moving forward a few minutes later, I had a complete scrubber that was complying with my needs. As you already suspect it, the above snippet uses macro too. But before getting into how the code works, i need to explain what quote and unquoted code is in Elixir.

Quote and unquote code

If the term scare you a little, don't worry it's the only concept of the article and it's easy to grasp.

Let's go back to the first snippet. Let's say that instead of generating a literal, i generate a method declaration. The following code wouldn't compile:

defmodule Sadraskol.Integers.Macro do
  defmacro def_two do
    def two, do: 2
  end
end

Instead, you have to quote the method declaration. It tells the preprocessor to treat the quoted code like a a representation of the Elixir code, not a expression to compile directly. You don't understand what I've just said, me neither. The official docs explains it better than i do. The final code you want is as follows: (the code generation equivalence is inlined each time):

defmodule Integers.Macro do
  defmacro def_two do
    quote do
      def two, do: 2
    end
  end
end
# ...
Integers.Macro.def_two
# Generates
def two, do: 2

Let's try to generalize the add_two with any number. The def_add_N macro would take the base_addend as argument and generate the according method declaration. With the information i provided you so far, you could think that the above implementation works:

defmodule Integers.Macro do
  defmacro def_add_N(base_addend) do
    quote do
      def add_N(addend), do: base_addend + addend
    end
  end
end
#...
Integers.Macro.def_add_N(2)
# Generates
def add_N(addend), do: base_addend + addend

Unfortunately, this code generates a compilation error, base_addend is undefined. This is where unquoting comes into play. It will take the value of an variable outside the quote scope and replace it.

defmodule Integers.Macro do
  defmacro def_add_N(base_addend) do
    quote do
      def add_N(addend), do: unquote(base_addend) + addend
    end
  end
end
#...
Integers.Macro.def_add_N(2)
# Generates
def add_N(addend), do: 2 + addend

We could stop here but my perfectionism wants to provide you with a useless but coherent code. We can introduce another parameter literal_name to produce different declarations:

defmodule Integers.Macro do
  defmacro def_add_N(name, base_addend) do
    quote do
      def unquote(:"add_#{name}")(addend), do: unquote(base_addend) + addend
    end
  end
end
#...
Integers.Macro.def_add_N "two", 2
# Generates
def add_two(addend), do: 2 + addend

What i used here is a little trick: in Elixir (and Erlang), all methods are identified by an atom, so i generate the atom in the unquoted code. As you can see unquoting can also be used to execute plain Elixir code within the macro.

Your turn to play

I've exposed here all i knew about generating code with Elixir macro. I hope that it has motivated you to dig a bit more into the language. As a cautious developer, I would not suggest you to use macro everywhere in your code since it could severely hinders readability.

Oh and for the explanation of the html_sanitize_ex snippet, i strongly suggest that you read the module defining the macro 🙂


Send your comments on twitter or by mail