You’re probably familiar with such popular backend frameworks as Python, Ruby, and PHP. However, these are not the last of their kind. Numerous alternatives have occurred, offering new opportunities, robust technologies, and efficient features. In particular, Elixir programming language is one of these dark horses, and many have already seen its great potential.
One of the keys to Elixir’s success and efficiency is macro — its core feature we will talk about in this article. Macros may seem too complex to understand and too risky to use. However, if applied wisely, they may reward you with numerous benefits and allow you to see the amazing world of metaprogramming from a new, clearer angle.
We at Wellnuts regularly deal with the Elixir framework to effectively serve our clients’ goals and build advanced, scalable systems. Thus, we will rely on our developers’ rich expertise in using macros.
This post will answer the following questions:
- What are Elixir macros?
- How to deal with metaprogramming in Elixir?
- What are the main specifics of the AST structure?
- How to write a simple macro?
- What else can you do with macros?
- What are the benefits and drawbacks of using Elixir macros?
What Are Elixir Macros?
So what are these mysterious macros, and when are they applicable?
In the Elixir framework, macros are advanced means for metaprogramming. They allow transforming the code and expanding a module. In other words, a macro is a tool allowing to write code that generates code.
Well, not that simple, of course. Since Elixir is a functional language, macros are not the code itself, being more comparable to functions. Therefore, they manipulate ASTs (Abstract Syntax Trees) for the sake of an effective compilation process.
To get the whole picture of macros, we should dive deeper into the metaprogramming concept, AST structure, and core Elixir specifics. So without further ado, let’s get started!
The Basics of Metaprogramming
As mentioned above, code can be generated by code at runtime, and we call this phenomenon metaprogramming. This approach allows using programs that can process other programs, interact with them, and modify them as if they were a part of their data structures.
Metaprogramming is quite complex, inconvenient, or even impossible in object-oriented programming languages such as Java, Ruby, C#, Python, etc. No, it doesn’t mean you can never turn to it when required. It just means that numerous obstacles might appear along the way, and there are multiple cases when metaprogramming is not an option.
However, such an approach is possible and quite helpful in functional languages like Elixir. It doesn’t involve any objects and modifies data structures in a more straightforward and dynamic way.
So to compare, in Ruby, where everything is an object, you should keep in mind all those methods and classes (that are still objects, anyway). So metaprogramming may turn from a useful tool into a true challenge.
In contrast, Elixir enables you to use metaprogramming as an easy and accessible way to move from runtime to compile-time, generate new code with the help of macros, and even create self-modifying code.
Metaprogramming in Elixir
Elixir’s metaprogramming relies entirely on the macros technique. With its help, you can use the existing features to create new ones. For that, you should change the AST structure in compile-time and modify it.
The following example presented in Elixir’s metaprogramming tutorial shows how to create a new module and implement the unless function.
defmodule Unless do def fun_unless(clause, do: expression) do if(!clause, do: expression) end defmacro macro_unless(clause, do: expression) do quote do if(!unquote(clause), do: unquote(expression)) end end end
AST (Abstract Syntax Tree)
An Abstract Syntax Tree represents a programming language’s syntax via a tree-like hierarchy. It is used to generate compliers’ symbol tables and generate code. So AST is the basis of the way compilers work.
AST is an internal code representation in most programming languages that doesn’t require the developers’ attention. You cannot change the source code and thus, do not care about its internal structure. However, in Elixir, everything’s different. The compiler makes ASTs accessible and allows interaction with them to build constructs and produce bytecode.
The structure of Elixir’s Abstract Syntax Tree consists of tuples explained below in more detail.
AST Structure: Tuples
The ASTs contain tuples which are the building blocks of Elixir’s data structures. Every tuple, in turn, contains three basic elements:
- An atom indicating the operation’s name.
- Metadata representation.
- Functions’ arguments (context).
To see how it works, you can use quote/2. And here’s what it reveals:
quote do: 42 42 iex> quote do: "Hello" "Hello" iex> quote do: :world :world iex> quote do: 1 + 2 {:+, [context: Elixir, import: Kernel], [1, 2]} iex> quote do: if value, do: "True", else: "False" {:if, [context: Elixir, import: Kernel], [{:value, [], Elixir}, [do: "True", else: "False"]]}
Thus, the compilation in Elixir allows transforming your source code into an AST before the bytecode creation.
At the same time, five literals do not change when applying quote/2:
- Atoms
- Numbers
- Lists
- Strings
- Two-element tuples
Here’s how the literals will differ from other data types in your code:
:atom iex> "string" "string" iex> 1 # All numbers 1 iex> [1, 2] # Lists [1, 2] iex> {"hello", :world} # 2 element tuples {"hello", :world}
quote and unquote Options
So as you see, we can use quote in Elixir to build an AST representation. To cut a long story short, here’s a simple example of how it works:
quote do 1 + 2 * 3 end {:+, [context: Elixir, import: Kernel], [1, {:*, [context: Elixir, import: Kernel], [2, 3]}]}
However, the critical goal of metaprogramming is not just generating ASTs but manipulating them. To make it possible, Elixir offers another convenient option — unquote.
You can use unquote to insert a particular value into the quote and trigger the required transformations in the code’s internal structure. Thus, it evaluates the argument and injects the obtained result into an AST. To see how it works, look at these examples provided by Elixir’s guidelines on metaprogramming.
Let’s try to inject the variable number into a quoted expression:
iex> number = 13 iex> Macro.to_string(quote do: 11 + number) "11 + number"
As you see, this is not the desired result. The variable hasn’t been injected, so we need to use unquote:
iex> number = 13 iex> Macro.to_string(quote do: 11 + unquote(number)) "11 + 13"
On top of that, you may use unquote as a tool to inject function names or even multiple values in the list. For that, you will need unquote_splicing.
Therefore, quote and unquote are useful for the developers working in Elixir and creating macros. They help transform and block code or make it generate code, which is the primary purpose of metaprogramming.
…And Once Again, Macros
Macros may seem too far from us by this time, but it was nearly impossible to get any closer. All those concepts, including metaprogramming, AST, and quote/unquote, are crucial components of the macros ecosystem in Elixir. So without understanding them, you will never get a complete picture of how macros work.
In the Elixir compilation process, macros are a part of the expansion. Therefore, this phase aims to receive the first AST, identify macros, and generate the final AST. Next, after some last operations, it comes to generating BEAM VM bytecode.
So based on all the above, macros help us get back the quoted expression and place it into the app’s code.
Here’s an example of how you can create a simple macro and use it for your needs. By the way, keep in mind that the macro should return the expression in the quote.
Let’s name this macro OurIf and give it a try.
defmodule NewIf do defmacro if?(condition, do: block, else: other) do quote do cond do unquote(condition) == true -> unquote(block) unquote(condition) == false -> unquote(other) end end end end iex(1)> require NewIf iex(2)> NewIf.if? 4 == 5, do: :yes, else: :no :
Each argument in this piece has its focus.
- condition serves for evaluation.
- do stops the execution process when condition is actual.
- else stops the execution process when condition is false
With this result, you can build your AST with the quote and inject the received values into it.
What Else You Can Do with Macros in Elixir
So we went through the basics of metaprogramming in Elixir. We’ve learned what macros are, how they work in the Elixir environment, and how you can build a simple macro on your own. However, it’s just the tip of the iceberg.
Since macros are probably Elixir’s most potent and specific feature, there are numerous helpful tricks and possible ways to use them.
Let’s look at the most notable ones.
Getting Rid of Bugs
Elixir has many macros and additional features, including checking AST chunks and converting AST to string. For this purpose, you can use Macro.to_string/2:
Macro.to_string(quote(do: foo.bar(1, 2, 3))) "foo.bar(1, 2, 3)"
It will provide you with the textual representation of your code and validate its behavior.
Expanding Macros
To expand the generated macros into the corresponding code, you can turn to Macro.expand/2 for recursive expansion and Macro.expand_once/2 for a one-time expansion. With their help, you will run macros on previously created ASTs and generate new ASTs in a more complete form.
Here’s how Macro.expand_once/2 works:
quoted |> Macro.expand_once(__ENV__) |> Macro.to_string |> IO.puts if(!true) do "Hi" end
And this is how you can expand your macros with Macro.expand/2:
quoted |> Macro.expand(__ENV__) |> Macro.to_string |> IO.puts case(!true) do x when x in [false, nil] -> nil _ -> "Hi" end
In our example, we expanded if macros into the corresponding case expression.
Creating Private Macros
You can also create private macros with defmacrop in Elixir, but it’s worth applying it carefully. The thing is, you should define the private macro before using it. Otherwise, it will fail to expand, and an error at the runtime will occur:
iex> defmodule Sample do ...> def four, do: two + two ...> defmacrop two, do: 2 ...> end ** (CompileError) iex:2: function two/0 undefined
So private macros are applicable only inside the defining module at compile time.
Using bind_quoted Option
Variable binding enables inserting multiple variables into your macro and ensuring the unquote function is applied only once. Otherwise, you may face a revaluation issue, where the system times in the output will be different. Here’s how bind_quoted works according to the Elixir School tutorial:
defmodule Example do defmacro double_puts(expr) do quote do IO.puts(unquote(expr)) IO.puts(unquote(expr)) end end end Example.double_puts(:os.system_time) 1450475941851668000 1450475941851733000
In contrast, when you apply bind, you will get the same time printed twice instead of two different times:
defmodule Example do defmacro double_puts(expr) do quote bind_quoted: [expr: expr] do IO.puts(expr) IO.puts(expr) end end end iex> require Example nil iex> Example.double_puts(:os.system_time) 1450476083466500000 1450476083466500000
Observing Macro Hygiene
In a nutshell, macro hygiene is how macros interact with the caller’s context during the expansion. It’s possible due to Elixir macros’ late resolution that ensures no conflicts between the defined variables inside the quote and the context may occur.
In Elixir, such protection is possible because it establishes a clear difference between macro and caller variables:
defmodule Example do defmacro hygienic do quote do: val = -1 end end iex> require Example nil iex> val = 42 42 iex> Example.hygienic -1 iex> val 42
At the same time, you can use var!/2 to invoke the unhygienic behavior of var. It will enable manipulating the value and establishing the connection between scopes bypassing macro hygiene:
defmodule Example do defmacro hygienic do quote do: val = -1 end defmacro unhygienic do quote do: var!(val) = -1 end end
However, such a trick may be risky as it often results in a resolution conflict between variables. Therefore, it’s worth trying not to overuse unhygienic operations in Elixir.
Elixir Macros and Their Pitfalls
So based on all the above, what stops us from writing macros and using them all along? Aren’t they a perfect solution that makes the developers’ lives easier?
Unfortunately, this statement is not so clear. Along with numerous benefits, macros can lead to specific challenges even in a dynamic programming language like Elixir.
Why? First of all, macros make your code less readable and clean. Of course, most developers strive for accessibility and performance speed when they create the program. However, it’s also worth remembering that a streamlined writing process is not always equal to simper perception. Moreover, in the case of macros, it’s precisely the opposite, especially if you overuse them in your code.
That’s why it’s a good idea to use macros wisely and responsibly. In particular, the developers should remember that this feature of the Elixir language is not equal to APIs. Thus, it would be best if you tried to avoid writing macros that will likely make testing and maintenance more complicated.
Therefore, not to turn an advantage into a downside, try to follow the fundamental principles of using Elixir macros:
- Strive for simplicity. Do not overwhelm your code with too many macros. Also, when you decide to apply them, make definitions and quoted content as brief as possible.
- Remember about hygiene. Macros Elixir hygiene provides some rules to ensure that the variables won’t directly impact the user code. So don’t overuse generating unhygienic macros too much since they may lead to resolution inconsistency.
- Pay attention to compile time and runtime contexts. When working with Elixir, you apply metaprogramming at compile time. So to write macros, you need to consider both compile time and runtime contexts. It may be challenging for beginners yet rewarding for those who have learned this Elixir’s features well.
Macros in Elixir: Pros and Cons
Finally, let’s consider all the main benefits you can get from macros using Elixir and the drawbacks you may face.
Elixir Macros Advantages
- Metaprogramming made easy. Macros are highly efficient, especially when it comes to metaprogramming. In some cases, they are even more result-driven than functions. For example, you can receive AST with macros instead of pre-evaluated expressions you can hardly use with functions.
- Hiding the code inside. The developers can hide the lines of their code by generating it inside a simple function definition.
- Accelerating the execution time. Compared to functions, macros work much faster. They save the time you might waste waiting for the compiler to invoke the function.
- Practical limitations provided by Elixir. Using macros may result in numerous risks. That is the case for many programming languages that do not limit their use and fail to prevent specific challenges. However, Elixir offers multiple default limitations that ensure responsible macros use. To name a few, these are macro hygiene, require and import modules to define macros, explicitly invoking mechanisms, and precise language of definitions.
Elixir Macros Disadvantages
- Macros lead to potential code difficulties. If overusing macros, you risk creating a code that looks bad and isn’t readable. Thus, it won’t be easy to test, maintain, and work with in the future. So it’s better to limit applying this feature and opt for it only when necessary.
- Macros are hard to learn. As you may have noticed, macros are a complicated and multilevel feature. They open up numerous opportunities yet require gaining sufficient expertise to use them effectively. And, of course, learning them will take a while. On top of that, Elixir is a relatively young programming language and a pretty distinctive one. So the newcomers will have to make a significant effort to master its capabilities.
Macros in Elixir: Use But Don’t Overuse
So, to sum up, macros are a great and compelling feature offered by Elixir. You can use them for numerous needs, especially when diving into metaprogramming. Furthermore, if you learn how macros work and what kind of benefits they provide, you can significantly streamline the development process. In particular, they will help you write code generating code, hide extra unnecessary lines behind definitions, and speed up the execution process.
However, it’s also critical not to use macros too much since they may affect readability, making testing and maintenance processes more challenging. So try to make it as simple as possible and apply macros only when it makes sense in your case.
Hopefully, our guide helped you gain some useful knowledge and didn’t leave more questions than answers.
However, if you want to learn more about the Elixir programming language and its features, we at Wellnuts can help you out.
Our company specializes in building scalable, data-driven solutions for different business needs. We are experts in IoT and provide full-cycle development services using multiple robust tools and frameworks. In particular, our backend engineers have well-established experience using Elixir, Phoenix, and more, to address our client’s needs.
Contact us, share your ideas, and get a detailed consultation on your project!
Ready to talk? Contact us: ask@wellnutscorp.com