Three Questions about Macros

I have been grokking Elixir projects on Github for a week since my last question in this forum… and as expected, more questions came up.

These questions arose because I come across quite a few projects, and quite big in size in terms of lines of code and file count, that their logic is written almost entirely with macros, and very few functions.

That prompted me a full day of learning macros from elixir-lang.org “Metaprogramming in Elixir” section, I ‘get it’ as far as syntax is concerned, but these questions are still not able to be answered:

1/ Is it idiomatic in Elixir to use macros?

2/ When to use macros? And when not to?

3/ How do you unit-test (and/or test in any other way) an application with its logic written almost entirely as macros?

1/ Is it idiomatic in Elixir to use macros?

Yes.

2/ When to use macros? And when not to?

When you can do it with a function, use a function.

3/ How do you unit-test (and/or test in any other way) an application with its logic written almost entirely as macros?

Techniques that work for functions do work for macros as well in many cases, but you need to differ between testing expansion of the macro and testing the runtime-bahviour of the expanded macro. But my personal opinion is that one shouldn’t test for the expansion but the runtime-bahviour only.

1/ Is it idiomatic in Elixir to use macros?
2/ When to use macros? And when not to?

The general concencus is: use macros when they make code more readable or manageable, for some definition of ‘readable’ and ‘manageable’.

There are many cases when it is better to not use macros, as they hide the details of what is going on underwater and thus are not explicit, which is something we strive to when writing Elixir code.

The exact yes/no therefore depends on your exact use case. A good rule-of-thumb might be to first try to implement something using normal functions, and only consider macros when you realize that there is a lot of boilerplate code neccesary for what you want to do.

Some use cases of Macros, to give you an idea of when/why they are used:

  • ExActor’s wrappers for the OTP GenServer behaviours. These macros abstract a lot of the (arguably archaic) inner workings of the GenServer callbacks and their return values for you, which makes your code a lot more readable.

  • The way Elixir handles Unicode Instead of writing all these function clauses by hand (which would be unmanageable as there are thousands of cases to consider), a file with unicode properties is read and converted into these function clauses at compile time.

  • Amnesia, which wraps the (arguably) archaic syntax that Erlang’s :mnesia module uses for its query specification, so you can write code that uses the simple Elixir operators.

  • ExUnit’s Assertions: By being able to read the AST before evaluating the tests, ExUnit is able to give much more descriptive error messages, something that would be impossible without macros.

3/ How do you unit-test (and/or test in any other way) an application with its logic written almost entirely as macros?

Just like any other function. After all, Macros are ‘just’ normal functions with the only difference being that they are passed an AST-fragment instead of evaluated input. You can write doctests, unit tests and integration tests just like you do for normal functions.

To add to the testing issue. Keep as much code as possible in regular functions, and call these from your macros. That means only a minimal part risks going wrong inside the macro; because no, macro errors are often not as easy to debug / test for… :sunglasses:

2 Likes