Meta Programming in Elixir
Meta Programming in Elixir
1
Meta-Programming in Elixir
Meta-Programming in Elixir
This file serves as your book's preface, a great place to describe your book's content and
ideas.
2
Quote and unquote
3
Quoting
Quoting
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:
You can get the representation of any expression by using the quote macro:
The first element is the function name, the second is a keyword list containing metadata and
the third is the arguments list.
Variables are also represented using such triplets, except 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 them
quoted expressions:
4
Quoting
Sometimes when working with quoted expressions, it may be useful to get the textual code
representation back. This can be done with Macro.to_string/1 :
In general, the tuples above are structured according to the following format:
Besides the tuple defined above, there are five Elixir literals that, when quoted, return
themselves (and not a tuple). They are:
Most Elixir code has a straight-forward translation to its underlying quoted expression. We
recommend you try out different code samples and see what the results are. For example,
what does String.upcase("foo") expand to? We have also learned that if(true,
do: :this, else: :that) is the same as if true do :this else :that end . How does this
affirmation hold with quoted expressions?
5
Unquoting
Unquoting
Quote is about retrieving the inner representation of some particular chunk of code.
However, sometimes it may be necessary to inject some other particular chunk of code
inside the representation we want to retrieve.
For example, imagine you have a variable number which contains the number you want to
inject inside a quoted expression.
iex> number = 13
iex> Macro.to_string(quote do: 11 + number)
"11 + number"
That's not what we wanted, since the value of the number variable has not been injected
and number has been quoted in the expression. In order to inject the value of the number
iex> number = 13
iex> Macro.to_string(quote do: 11 + unquote(number))
"11 + 13"
In some cases, it may be necessary to inject many values inside a list. For example, imagine
you have a list containing [1, 2, 6] and we want to inject [3, 4, 5] into it. Using
unquote won't yield the desired result:
6
Unquoting
Unquoting is very useful when working with macros. When writing macros, developers are
able to receive code chunks and inject them inside other code chunks, which can be used to
transform code or write code that generates code during compilation.
7
Escaping
Escaping
As we saw at the beginning of this chapter, only some values are valid quoted expressions
in Elixir. For example, a map is not a valid quoted expression. Neither is a tuple with four
elements. However, such values can be expressed as a quoted expression:
In some cases, you may need to inject such values into quoted expressions. To do that, we
need to first escape those values into quoted expressions with the help of Macro.escape/1 :
Macros receive quoted expressions and must return quoted expressions. However,
sometimes during the execution of a macro, you may need to work with values and making a
distinction between values and quoted expressions will be required.
In other words, it is important to make a distinction between a regular Elixir value (like a list,
a map, a process, a reference, etc) and a quoted expression. Some values, such as
integers, atoms and strings, have a quoted expression equal to the value itself. Other
values, like maps, need to be explicitly converted. Finally, values like functions and
references cannot be converted to a quoted expression at all.
You can read more about quote and unquote in the Kernel.SpecialForms module.
Documentation for Macro.escape/1 and other functions related to quoted expressions can
be found in the Macro module.
In this introduction we have laid the groundwork to finally write our first macro, so let's move
to the next chapter.
8
Macros
Macros
9
Foreword
Foreword
Even though Elixir attempts its best to provide a safe environment for macros, the major
responsibility of writing clean code with macros falls on developers. Macros are harder to
write than ordinary Elixir functions and it's considered to be bad style to use them when
they're not necessary. So write macros responsibly.
Elixir already provides mechanisms to write your every day code in a simple and readable
fashion by using its data structures and functions. Macros should only be used as a last
resort. Remember that explicit is better than implicit. Clear code is better than concise
code.
10
Our first macro
For this chapter, we will be using files instead of running code samples in IEx. That's
because the code samples will span multiple lines of code and typing them all in IEx
can be counter-productive. You should be able to run the code samples by saving them
into a macros.exs file and running it with elixir macros.exs or iex macros.exs .
In order to better understand how macros work, let's create a new module where we are
going to implement unless , which does the opposite of if , as a macro and as a function:
defmodule Unless do
def fun_unless(clause, expression) do
if(!clause, do: expression)
end
The function receives the arguments and passes them to if . However, as we learned in
the previous chapter, the macro will receive quoted expressions, inject them into the quote,
and finally return another quoted expression.
$ iex macros.exs
11
Our first macro
Note that in our macro implementation, the sentence was not printed, although it was printed
in our function implementation. That's because the arguments to a function call are
evaluated before calling the function. However, macros do not evaluate their arguments.
Instead, they receive the arguments as quoted expressions which are then transformed into
other quoted expressions. In this case, we have rewritten our unless macro to become an
if behind the scenes.
macro_unless(true, {{:., [], [{:aliases, [], [:IO]}, :puts]}, [], ["this should never
be printed"]})
{:if, [], [
{:!, [], [true]},
{{:., [], [IO, :puts], [], ["this should never be printed"]}}]}
iex> expr = quote do: Unless.macro_unless(true, IO.puts "this should never be printed"
)
iex> res = Macro.expand_once(expr, __ENV__)
iex> IO.puts Macro.to_string(res)
if(!true) do
IO.puts("this should never be printed")
end
:ok
That's what macros are all about. They are about receiving quoted expressions and
transforming them into something else. In fact, unless/2 in Elixir is implemented as a
macro:
12
Our first macro
Constructs such as unless/2 , defmacro/2 , def/2 , defprotocol/2 , and many others used
throughout this getting started guide are implemented in pure Elixir, often as a macros. This
means that the constructs being used to build the language can be used by developers to
extend the language to the domains they are working on.
We can define any function and macro we want, including ones that override the built-in
definitions provided by Elixir. The only exceptions are Elixir special forms which are not
implemented in Elixir and therefore cannot be overridden, the full list of special forms is
available in Kernel.SpecialForms .
13
Macros hygiene
Macros hygiene
Elixir macros have late resolution. This guarantees that a variable defined inside a quote
won't conflict with a variable defined in the context where that macro is expanded. For
example:
defmodule Hygiene do
defmacro no_interference do
quote do: a = 1
end
end
defmodule HygieneTest do
def go do
require Hygiene
a = 13
Hygiene.no_interference
a
end
end
HygieneTest.go
# => 13
In the example above, even though the macro injects a = 1 , it does not affect the variable
a defined by the go function. If a macro wants to explicitly affect the context, it can use
var! :
defmodule Hygiene do
defmacro interference do
quote do: var!(a) = 1
end
end
defmodule HygieneTest do
def go do
require Hygiene
a = 13
Hygiene.interference
a
end
end
HygieneTest.go
# => 1
14
Macros hygiene
Variable hygiene only works because Elixir annotates variables with their context. For
example, a variable x defined on line 3 of a module would be represented as:
defmodule Sample do
def quoted do
quote do: x
end
end
Notice that the third element in the quoted variable is the atom Sample , instead of nil ,
which marks the variable as coming from the Sample module. Therefore, Elixir considers
these two variables as coming from different contexts and handles them accordingly.
Elixir provides similar mechanisms for imports and aliases too. This guarantees that a macro
will behave as specified by its source module rather than conflicting with the target module
where the macro is expanded. Hygiene can be bypassed under specific situations by using
macros like var!/2 and alias!/2 , although one must be careful when using those as they
directly change the user environment.
Sometimes variable names might be dynamically created. In such cases, Macro.var/2 can
be used to define new variables:
defmodule Sample do
defmacro initialize_to_char_count(variables) do
Enum.map variables, fn(name) ->
var = Macro.var(name, nil)
length = name |> Atom.to_string |> String.length
quote do
unquote(var) = unquote(length)
end
end
end
def run do
initialize_to_char_count [:red, :green, :yellow]
[red, green, yellow]
end
end
15
Macros hygiene
Take note of the second argument to Macro.var/2 . This is the context being used and will
determine hygiene as described in the next section.
16
The environment
The environment
When calling Macro.expand_once/2 earlier in this chapter, we used the special form
__ENV__ .
__ENV__ returns an instance of the Macro.Env struct which contains useful information
about the compilation environment, including the current module, file and line, all variables
defined in the current scope, as well as imports, requires and so on:
iex> __ENV__.module
nil
iex> __ENV__.file
"iex"
iex> __ENV__.requires
[IEx.Helpers, Kernel, Kernel.Typespec]
iex> require Integer
nil
iex> __ENV__.requires
[IEx.Helpers, Integer, Kernel, Kernel.Typespec]
Many of the functions in the Macro module expect an environment. You can read more
about these functions in the docs for the Macro module and learn more about the
compilation environment in the docs for Macro.Env .
17
Private macros
Private macros
Elixir also supports private macros via defmacrop . As private functions, these macros are
only available inside the module that defines them, and only at compilation time.
It is important that a macro is defined before its usage. Failing to define a macro before its
invocation will raise an error at runtime, since the macro won't be expanded and will be
translated to a function call:
18
Write macros responsibly
Macros are hygienic: by default, variables defined inside a macro are not going to affect
the user code. Furthermore, function calls and aliases available in the macro context
are not going to leak into the user context.
Macros are lexical: it is impossible to inject code or macros globally. In order to use a
macro, you need to explicitly require or import the module that defines the macro.
Macros are explicit: it is impossible to run a macro without explicitly invoking it. For
example, some languages allow developers to completely rewrite functions behind the
scenes, often via parse transforms or via some reflection mechanisms. In Elixir, a macro
must be explicitly invoked in the caller during compilation time.
Macros' language is clear: many languages provide syntax shortcuts for quote and
unquote . In Elixir, we preferred to have them explicitly spelled out, in order to clearly
delimit the boundaries of a macro definition and its quoted expressions.
Even with such guarantees, the developer plays a big role when writing macros responsibly.
If you are confident you need to resort to macros, remember that macros are not your API.
Keep your macro definitions short, including their quoted contents. For example, instead of
writing a macro like this:
defmodule MyModule do
defmacro my_macro(a, b, c) do
quote do
do_this(unquote(a))
...
do_that(unquote(b))
...
and_that(unquote(c))
end
end
end
write:
19
Write macros responsibly
defmodule MyModule do
defmacro my_macro(a, b, c) do
quote do
# Keep what you need to do here to a minimum
# and move everything else to a function
do_this_that_and_that(unquote(a), unquote(b), unquote(c))
end
end
def do_this_that_and_that(a, b, c) do
do_this(a)
...
do_that(b)
...
and_that(c)
end
end
This makes your code clearer and easier to test and maintain, as you can invoke and test
do_this_that_and_that/3 directly. It also helps you design an actual API for developers that
do not want to rely on macros.
With those lessons, we finish our introduction to macros. The next chapter is a brief
discussion on DSLs that shows how we can mix macros and module attributes to annotate
and extend modules and functions.
20
Domain Specific Languages
21
Foreword
Foreword
Domain Specific Languages (DSL) allow developers to tailor their application to a particular
domain. You don't need macros in order to have a DSL: every data structure and every
function you define in your module is part of your Domain Specific Language.
For example, imagine we want to implement a Validator module which provides a data
validation domain specific language. We could implement it using data structures, functions
or macros. Let's see how those different DSLs would look like:
# 2\. functions
import Validator
user
|> validate_length(:name, 1..100)
|> validate_matches(:email, ~r/@/)
MyValidator.validate(user)
Of all the approaches above, the first is definitely the most flexible. If our domain rules can
be encoded with data structures, they are by far the easiest to compose and implement, as
Elixir's standard library is filled with functions for manipulating different data types.
The second approach uses function calls which better suits more complex APIs (for
example, if you need to pass many options) and reads nicely in Elixir thanks to the pipe
operator.
The third approach, uses macros, and is by far the most complex. It will take more lines of
code to implement, it is hard and expensive to test (compared to testing simple functions),
and it limits how the user may use the library since all validations need to be defined inside a
module.
22
Foreword
To drive the point home, imagine you want to validate a certain attribute only if a given
condition is met. We could easily achieve it with the first solution, by manipulating the data
structure accordingly, or with the second solution by using conditionals (if/else) before
invoking the function. However it is impossible to do so with the macros approach unless its
DSL is augmented.
In other words:
That said, there are still cases where using macros and modules to build domain specific
languages is useful. Since we have explored data structures and function definitions in the
Getting Started guide, this chapter will explore how to use macros and module attributes to
tackle more complex DSLs.
23
Building our own test case
defmodule MyTest do
use TestCase
MyTest.run
In the example above, by using TestCase , we can write tests using the test macro, which
defines a function named run to automatically run all tests for us. Our prototype will simply
rely on the match operator ( = ) as a mechanism to do assertions.
24
The test macro
defmodule TestCase do
# Callback invoked by `use`.
#
# For now it simply returns a quoted expression that
# imports the module itself into the user code.
@doc false
defmacro __using__(_opts) do
quote do
import TestCase
end
end
@doc """
Defines a test case with the given description.
## Examples
"""
defmacro test(description, do: block) do
function_name = String.to_atom("test " <> description)
quote do
def unquote(function_name)(), do: unquote(block)
end
end
end
25
The test macro
For now we don't have a mechanism to run tests, but we know that a function named "test
hello" was defined behind the scenes. When we invoke it, it should fail:
26
Storing information with attributes
When discussing module attributes in earlier chapters, we mentioned how they can be used
as temporary storage. That's exactly the property we will apply in this section.
27
Storing information with attributes
defmodule TestCase do
@doc false
defmacro __using__(_opts) do
quote do
import TestCase
@doc """
Defines a test case with the given description.
## Examples
"""
defmacro test(description, do: block) do
function_name = String.to_atom("test " <> description)
quote do
# Prepend the newly defined test to the list of tests
@tests [unquote(function_name) | @tests]
def unquote(function_name)(), do: unquote(block)
end
end
By starting a new IEx session, we can now define our tests and run them:
28
Storing information with attributes
Although we have overlooked some details, this is the main idea behind creating domain
specific modules in Elixir. Macros enable us to return quoted expressions that are executed
in the caller, which we can then use to transform code and store relevant information in the
target module via module attributes. Finally, callbacks such as @before_compile allow us to
inject code into the module when its definition is complete.
Besides @before_compile , there are other useful module attributes like @on_definition and
@after_compile , which you can read more about in the docs for the Module module. You
can also find useful information about macros and the compilation environment in the
documentation for the Macro module and Macro.Env .
29