Welcome to a tutorial on Macros in Elixir.
In Elixir, Macros are one of the most advanced and powerful features. Macros should be sparingly used just like any other advanced features programming language.
Macros make it possible to perform powerful code transformations in compilation time.
Let’s look at the Elixir internals before we proceed with macros.
Any Elixir program can be represented by its own data structures. The building block of an Elixir program is a tuple with three elements, e.g., the function call sum(1, 2, 3) is represented internally as:
{:sum, [], [1, 2, 3]}
In the above example, the first element is the function name, the second is a keyword list containing metadata and the third is the arguments list. This can be obtained as the output in iex shell if the code below is written.
quote do: sum(1, 2, 3)
Also, operators are represented as such tuples. While, variables are also represented using such triplets, except that the last element is an atom, instead of a list. However, when quoting more complex expressions, it is obvious 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), but Elixir calls these quoted expressions.
Since we have known how to retrieve the internal structure of our code, we need to learn how to modify it. To inject new code or values, the unquote is used. When we unquote an expression it will be evaluated and then injected into the AST(Abstract Syntax Tree) check out the example below in iex shell.
num = 25
quote do: sum(15, num)
quote do: sum(15, unquote(num))
The output is:
{:sum, [], [15, {:num, [], Elixir}]}
{:sum, [], [15, 25]}
In the above example quote expression, the code did not automatically replace num with 25. Thus, we need to unquote this variable if we want to modify the AST.
Since you are familiar with quote and unquote, let’s explore metaprogramming in Elixir using macros.
Macros in the simplest term, are special functions designed to return a quoted expression that will be inserted into our application code. Let's Imagine that the macro is replaced with the quoted expression rather than called a function. With the application of macros, we have everything necessary to extend Elixir and dynamically add code to our applications
Now, let’s implement unless as a macro. We will start by defining the macro using the defmacro macro. Don’t forget that our macro needs to return a quoted expression. This is shown below.
defmodule OurMacro do
defmacro unless(expr, do: block) do
quote do
if !unquote(expr), do: unquote(block)
end
end
end
require OurMacro
OurMacro.unless true, do: IO.puts "True Expression"
OurMacro.unless false, do: IO.puts "False expression"
The output is:
False expression
What happened above, is that our code is being replaced by the quoted code returned by the unless macro. We have unquoted the expression to evaluate it in the current context and also unquoted the do block in order to execute it in its context. The above example shows us metaprogramming using macros in Elixir.
In addition, Macros can be used in much more complex tasks but should be used sparingly, as said earlier. It is because metaprogramming in general is considered a bad practice and should be used only when necessary.