Jorge Chávez

@jorgech

Understanding Elixir Macros

Some time ago when I was studying Elixir for the first time, I step out with a chapter in the documentation related with something called Macros. When I looked into this, some complicated terms started to show up and then I got scared. Things like:

  • Metaprogramming
  • Abstract Syntax Three
  • Elixir AST
  • AST Literals
  • quote & unquote

In order to understand Macros we should have an idea of what these new technical terms means, and how they are related with Elixir.

Metaprogramming

Almost all blogs, books or videos will say something like Code that writes code, but I really liked the explanation that @iamvery gave in one of his talks.

Metaprogramming it’s just programming …
Programming is performing some work in some data …
Data are quantities on which operations are performed.

So metaprogramming is like programming regularly. However the data where we perform operations can be whatever we want and not just the data structures that the language offers us. For example:

2 + 3

We know that ‘2 and ‘3 are data and ‘+’ is the operation. However in metaprogramming ‘+’ is considered data too. We can manipulate that, have access to it and do whatever we want with it.

Abstract Syntax Three

When we make programs, the compiler in charge of transforming our code into bytecode needs to know the syntactic structure of our program, so the instructions can be performed as we defined. An abstract syntax three is that representation. Let see some really simple examples:

2 + 3
AST (Abstract Syntax Tree)
2 + 3 == 5
AST (Abstract Syntax Tree)

Elixir AST

Elixir has it’s own way to represent AST, this is by converting almost everything to a three element tuple, just to recap a little bit on this, the Elixir compiler has some steps before it produces the .beam file, this is a really brief schema:

Elixir compiler phases

When using macros we are scripting and producing Elixir AST, we won’t touch the Expand Macros and Erlang AST. But, it’s good to know that those steps are there.

AST Literals

Basic data structures like atom, integer, float, string, list and two element tuples are considered literals. It means that when the compiler process that, they are the the same in AST form. Example:

AST Literal

3 element tuples

In the other hand, everything else is transformed to a three element tuple which has this basic structure:

If it is a variable:

First element is the variable name, second item is the metadata that it’s needed in order to this variable to exist (normally empty), and the last item is the context.

Variable to Elixir AST

If it is a call

First element is the function name, second is just metadata (normally you won’t care about this), and last item are the arguments.

Function call to Elixir AST

quote

Probably at this point you are thinking on quitting this article, AST are complicated nested structures. However, there are some helpful functions (actually they are Macros too) that can help us manage all this structures.

May be if you see an example of quote, you can understand what it is and how to use it:

iex> quote do: 2 + 3
{:+, [context: Elixir, import: Kernel], [2, 3]}

we can use it inline or multiline as any other function or macro.

iex(1)> quote do
...(2)> if 2 + 3 == 5 do
...(3)> "this is true"
...(4)> else
...(5)> "this is false"
...(6)> end
...(7)> end
{:if, [context: Elixir, import: Kernel],
[{:==, [context: Elixir, import: Kernel],
[{:+, [context: Elixir, import: Kernel], [2, 3]}, 5]},
[do: "this is true", else: "this is false"]]}

unquote

With unquote, the first thing that went to my mind was:

iex(1)> unquote do: {:+, [context: Elixir, import: Kernel], [2, 3]}
"2 + 3"

but that is not how unquote works, let’s put it this way.

quote and unquote are like “interpolated strings”

Imagine the next code:

iex(1)> name = "George"
"George"
iex(2)> "Hello name"
"Hello name"

We need to evaluate the variable name, with strings we use #{}

iex(1)> name = "George"
"Geroge"
iex(2)> "Hello #{name}"
"Hello George"

With quoted expressions we use unquote.

iex(1)> number = 3
iex(2)> quote do: 2 + unquote(number)
{:+, [context: Elixir, import: Kernel], [2, 3]}

Macros

Finally, the main idea of this article is to show you what Macros are but as I said in the beginning, there are some other concepts that we needed to understand in order to write Macros.

Let’s define our first Macro:

defmodule MyMacros do
  defmacro nice_print({:+, _meta, [lhs, rhs]}) do
quote do
IO.puts """
#{unquote(lhs)}
+ #{unquote(rhs)}
--
#{unquote(lhs+rhs)}
"""
end
end
end

See how immediately we use quote, that’s because macros always return an Elixir AST, if you don’t you will get an error.

Let’s use that Macro:

iex(1)> require MyMacros
[MyMacros]
iex(2)> MyMacros.nice_print 2 + 3
2
+ 3
--
5

Notice that our macro receives a three element tuple, that’s the Elixir AST of 2 + 3 in this case and not the evaluation of that expression which is 5.
quote & unquote are not the only tools that we have in order to manage Macros.

Macro.to_string

iex(1)> expression = quote do: 2 + 3
{:+, [context: Elixir, import: Kernel], [2, 3]}
iex(2)> Macro.to_string(expression)
“2 + 3”

Code.eval_quoted

iex(1)> expression = quote do: 2 + 3
{:+, [context: Elixir, import: Kernel], [2, 3]}
iex(2)> Code.eval_quoted(expression)
{5, []}

Pros & Cons

In Chris McCord book “Metaprogramming Elixir” he has a section called Macro Rules, and his first rule is:

Don’t Write Macros

But why?, they are powerful and amazing, aren’t it?… I guess that’s the first reason, they are so powerful that you can get addicted to overuse them and by overuse them you code can look ugly and hard to read.

In one of the Jesse Anderson talks he show one really interesting quote:

Code is read many more times than it is written, which means that the ultimate cost of code is in its reading.

And for me that’s true, we don’t have to lose the idea to writing code beautiful and easy to read.

The benefits of using Macros are the metaprogramming part, we can do things that with normal functions we can’t, arguments in functions are evaluated before we can even use them in our function body, with Macros this is not the case since we only receive the Elixir AST version of our expressions.

Macros are also good for hiding boilerplate, imagine that you had to write this

case(true) do
x when x in [false, nil] ->
false
_ ->
true
end

instead of this

if true do
true
else
false
end

So, with macros we have this possibility of hiding code by generating it inside the macro.

Hopefully you get the idea of what Macros are and how to use them, for me this topic is really awesome since this is the first time I do metaprogramming. Elixir make it easy and maintainable to read. If you have any question here is my twitter @jorgechavz. Thanks!

Resources

Macros don’t stop here, there is so many information out there related with this topic, I will leave some links here that really helped me to understand this interesting topic.

The minimum knowledge you need to start Metaprogramming in Elixir
https://dockyard.com/blog/2016/08/16/the-minumum-knowledge-you-need-to-start-metaprogramming-in-elixir

Don’t Write Macros But Do Learn How They Work — Jesse Anderson https://www.youtube.com/watch?v=Bo48sQDb-hk

Write less, do more (and have fun!) with elixir macros
Video:
https://www.youtube.com/watch?v=mkoYqXdXl5Y
Slides: http://slides.com/chrismccord/elixir-macros#/

Understanding Elixir Macros
http://theerlangelist.com/article/macros_1

Topics of interest

More Related Stories