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.
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:
macro
can inflate compilation time by a lotWe won't cover those as they are specific issues that could be very dependent on the context or subjective perception.
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.
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.
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
.
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 🙂