Elixir - Macros


Macros are one of the most advanced and powerful features of Elixir. As with all advanced features of any language, macros should be used sparingly. They make it possible to perform powerful code transformations in compilation time. We will now understand what macros are and how to use them in brief.


Before we start talking about macros, let us first look at Elixir internals. An Elixir program can be represented by its own data structures. The building block of an Elixir program is a tuple with three elements. For example, the function call sum(1, 2, 3) is represented internally as −

{:sum, [], [1, 2, 3]}

The first element is the function name, the second is a keyword list containing metadata and the third is the arguments list. You can get this as the output in iex shell if you write the following −

quote do: sum(1, 2, 3)

Operators are also represented as such tuples. Variables are also represented using such triplets, except that the last element is an atom, instead of a list. When quoting more complex expressions, we can see that the code is represented in such tuples, which are often nested inside each other in a structure resembling a tree. Many languages would call such representations an Abstract Syntax Tree (AST). Elixir calls these quoted expressions.


Now that we can retrieve the internal structure of our code, how do we modify it? To inject new code or values, we use unquote. When we unquote an expression it will be evaluated and injected into the AST. Let us consider an example(in iex shell) to understand the concept −

num = 25

quote do: sum(15, num)

quote do: sum(15, unquote(num))

When the above program is run, it produces the following result −

{:sum, [], [15, {:num, [], Elixir}]}
{:sum, [], [15, 25]} 

In the example for the quote expression, it did not automatically replace num with 25. We need to unquote this variable if we want to modify the AST.


So now that we are familiar with quote and unquote, we can explore metaprogramming in Elixir using macros.

In the simplest of terms macros are special functions designed to return a quoted expression that will be inserted into our application code. Imagine the macro being replaced with the quoted expression rather than called like a function. With macros we have everything necessary to extend Elixir and dynamically add code to our applications

Let us implement unless as a macro. We will begin by defining the macro using the defmacro macro. Remember that our macro needs to return a quoted expression.

defmodule OurMacro do
   defmacro unless(expr, do: block) do
      quote do
         if !unquote(expr), do: unquote(block)

require OurMacro

OurMacro.unless true, do: IO.puts "True Expression"

OurMacro.unless false, do: IO.puts "False expression"

When the above program is run, it produces the following result −

False expression 

What is happening here is our code is being replaced by the quoted code returned by the unless macro. We have unquoted the expression to evaluate it in current context and also unquoted the do block to execute it in its context. This example shows us metaprogramming using macros in elixir.

Macros can be used in much more complex tasks but should be used sparingly. This is because metaprogramming in general is considered a bad practice and should be used only when necessary.