This may be a stupid question, but I’m wondering if anyone has a good checklist or decision-tree type of list that would help a developer decide whether to implement a bit of functionality as a macro instead of as a function?
I read that something as simple as is_integer/1 is actually a macro (because it can be used in a guard clause), but the source code seems to define it as a regular function:
@doc guard: true
@spec is_integer(term) :: boolean
def is_integer(term) do
So far in my Elixir career, I don’t think I’ve authored a single macro (excluding following along with tutorials). The closest I’ve come is writing custom guard clauses, and maybe that hints at one of the reasons why you would choose a macro over a function: macros are evaluated at compile time (or… something like that?) so you can use them in certain places where regular functions would not work. Is this “order-of-operations” during compilation the biggest consideration?
The function is_integer/1 is inlined by the Elixir compiler. :erlang.is_integer/1 is a guard BIF. These type of functions can be used in guards. I played a little with Elixir before deciding to stick with Erlang, so I don’t have the best info on what Elixir macros are really good for. I only ever used them to create custom guards.
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 everyday 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.
So in general, don’t write macros if there is another way to express yourself.
The only macro in current project:
For background jobs, generate perform() function with required number of arguments, that wraps actual useful function with a bunch of standard code: capture exceptions, log stuff etc. Since there a lot of worker modules it removes duplication of code. Also no worries duplicated code will be different across modules
The ideal time to reach for macros is when you’re building something that needs their specific “code that writes code” behavior. For instance, the macros of Ecto.Schema build a set of compile-time data structures that eventually define the __schema__/1 function: