Skip to content

Meta-programming anti-patterns ​

Source: Elixir guide, Meta-programming anti-patterns.

Anti-patterns about macros and code generation. Five of them.

Compile-time dependencies ​

Problem: any macro use adds a compile-time dependency on the module defining the macro. Worse, when a macro is used in a module body, its arguments can become compile-time dependencies too, so changing one file recompiles many.

elixir
defmodule MyApp do
  use Plug.Builder
  plug MyApp.Authentication
end

plug stores the module in @plugs, and MyApp.Authentication is only called at runtime. But because plug ran at compile time, it is now a compile-time dependency of MyApp.

Refactoring: expand the literal as if it were inside the function where it's used:

elixir
defmacro plug(mod) do
  mod = Macro.expand_literals(mod, %{__CALLER__ | function: {:call, 2}})
  quote do
    @plugs unquote(mod)
  end
end

Now it's a runtime dependency. Only do this if the macro doesn't call functions, read structs or inspect the module at compile time. Use mix xref trace path/to/file.ex to see a file's compile-time, runtime and export dependencies.

A compile-time dependency drags its own dependencies into recompilation.

Large code generation ​

Problem: a macro that expands into a lot of code makes compilation slower and the artifacts larger, because it's expanded and compiled at every call, and a router can have hundreds of get/2.

Refactoring: keep the quote tiny and delegate the work to a function:

elixir
defmacro get(route, handler) do
  quote do
    Routes.__define__(__MODULE__, unquote(route), unquote(handler))
  end
end

def __define__(module, route, handler) do
  # checks, raises, Module.put_attribute(...)
end
The checks are compiled once, not once per route.

Unnecessary macros ​

Problem: a macro where a function would do makes code harder to read and reason about, and harder to evolve.

elixir
defmacro sum(v1, v2) do
  quote do: unquote(v1) + unquote(v2)
end

Refactoring: a plain function, with no require needed:

elixir
def sum(v1, v2), do: v1 + v2

This is the guidance from the Macros chapter: macros are a last resort.

use instead of import ​

Problem: import and alias are lexical and only let a module call another. use lets a module inject any code, including propagated imports, so you must know the library's internals to know what your module now contains.

elixir
defmodule Library do
  defmacro __using__(_opts) do
    quote do
      import Library
      import ModuleA   # propagated
    end
  end
end

defmodule ClientApp do
  use Library
  def foo, do: "local"   # error: imported ModuleA.foo/0 conflicts with local function
end

Refactoring: avoid __using__/1 when alias or import would do. ClientApp just does import Library.

use hides what gets injected. import shows it.

When you truly need more, use is the common extension point. Document its effects in @moduledoc like a nutrition label, listing only changes to the public API. For example: When you use GenServer, the GenServer module will set @behaviour GenServer and define a child_spec/1 function, so your module can be used as a child in a supervision tree.

Untracked compile-time dependencies ​

Problem: the opposite: a compile-time dependency the compiler can't see, so it doesn't recompile when it should. It happens when module names are built dynamically.

elixir
mods = [OtherModule.Foo, OtherModule.Bar]     # fine: literals are tracked
for mod <- mods, do: mod.example()

for part <- [:Foo, :Bar] do                   # bad: the compiler only sees OtherModule
  Module.concat(OtherModule, part).example()
end

Writing :"Elixir.OtherModule.Foo" atoms directly has the same problem, since Elixir never sees the aliases.

Only literal aliases are tracked.

Refactoring: use full module names. If you must build them, do it in a macro at compile time so the compiler still sees them:

elixir
defmacro call_examples(parts) do
  for part <- parts do
    quote do
      OtherModule.unquote(part).example()
    end
  end
end

mix xref trace helps check that dependencies are tracked.

Site code: MIT. Pages and diagrams are derived from the Elixir documentation (Apache-2.0). Not affiliated with the Elixir Team.