Why do we use macros in elixir?

I have seen macros being used in a lot of code at different parts but never really understood the thought process behind it. Can someone please list some examples with some thought process behind using macro ?

Can you add details to your question, are you interested in how macros work or what are the main use-cases?

Please give details on both

a good entry point is here Macros - The Elixir programming language, but also point 1 and 3 of the meta programming section

2 Likes

First thing to mention is when NOT to use a macro, that is, almost always. You only want to reach for a macro when you have no choice but to use one. See Library Guidelines — Elixir v1.15.5

There are several reasons you might need a macro, but almost all of them are for libraries, not your typical application code:

  • you need to do something at compile time, e.g. defining a function (defn in Nx, Plug.Router…), which regular functions cannot do

  • you want a specific DSL like Ecto.Query

  • you want a syntax to generate a data structure that also works in patterns/guards (../2 for ranges, Date/Time sigils like ~D…). You can do a..b = 1..10, but functions cannot be called in patterns/guards.

  • you need to access the AST, e.g. assert/1: if it was a function, it would only see the final value (false), not the operation you’re trying to do (>, ==…), the operands (left and right), or the code it is printing (code: assert 1 + 2 + 3 + 4 > 15).

  • performance/optimization: Logger could typically be implemented with functions, but we want to only evaluate the message if the logger level is above the configured level, and an API like Logger.maybe_info(fn -> "hello #{world}" end) would have a bigger runtime footprint (plus a more clunky API). Instead, we generate efficient code by checking the log level at compile time.

12 Likes

For an example the if/2 in Elixir is a macro that expands to a case/2. You can see this if you disassemble the code from the BEAM file.

# macro.exs
Mix.install([{:beam_file, "~> 0.5.3"}])

defmodule Foo do
  def pos(x) do
    if x > 0 do
      "yes"
    else
      "no"
    end
  end
end
|> BeamFile.elixir_code!()
|> IO.iodata_to_binary()
|> IO.puts()
> elixir macro.exs
defmodule Elixir.Foo do
  def pos(x) do
    case :erlang.>(x, 0) do
      false -> "no"
      true -> "yes"
    end
  end
end