Skip to content

Commit 66f0bd0

Browse files
authored
Add anti-pattern on compile-time dependency
1 parent 17a0130 commit 66f0bd0

File tree

1 file changed

+71
-0
lines changed

1 file changed

+71
-0
lines changed

lib/elixir/pages/anti-patterns/macro-anti-patterns.md

+71
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,77 @@
22

33
This document outlines potential anti-patterns related to meta-programming.
44

5+
## Compile-time dependencies
6+
7+
#### Problem
8+
9+
This anti-pattern is related to dependencies between files in Elixir. Because macros are used at compile-time, the use of any macro in Elixir adds a compile-time dependency to the module that defines the macro.
10+
11+
However, when macros are used in the body of a module, the arguments to the macro themselves may become compile-time dependencies. These dependencies may lead to dependency graphs where changing a single file causes several files to be recompiled.
12+
13+
#### Example
14+
15+
Let's take the [`Plug`](https://github.com/elixir-plug/plug) library as an example. The `Plug` project allows you specify several modules, also known as plugs, which will be invoked whenever there is a request. As a user of `Plug`, you would use it as follows:
16+
17+
```elixir
18+
defmodule MyApp do
19+
use Plug.Builder
20+
21+
plug MyApp.Authentication
22+
end
23+
```
24+
25+
And imagine `Plug` has the following definitions of the macros above (simplified):
26+
27+
```elixir
28+
defmodule Plug.Builder do
29+
defmacro __using__(_opts) do
30+
quote do
31+
Module.register_attribute(__MODULE__, :plugs, accumulate: true)
32+
@before_compile Plug.Builder
33+
end
34+
end
35+
36+
defmacro plug(mod) do
37+
quote do
38+
@plugs unquote(mod)
39+
end
40+
end
41+
42+
...
43+
end
44+
```
45+
46+
The implementation accumulates all modules inside the `@plugs` module attribute. Right before the module is compiled, `Plug.Builder` will reads all modules stored in `@plugs` and compile them into a function, like this:
47+
48+
```elixir
49+
def call(conn, _opts) do
50+
MyApp.Authentication.call(conn)
51+
end
52+
```
53+
54+
The trouble with the code above is that, because the `plug MyApp.Authentication` was invoked at compile-time, the module `MyApp.Authentication` is now a compile-time dependency of `MyApp`, even though `MyApp.Authentication` is never used at compile-time. If `MyApp.Authentication` depends on other modules, even at runtime, this can now lead to a large recompilation graph in case of changes.
55+
56+
#### Refactoring
57+
58+
To address this anti-pattern, a macro can expand literals within the context they are meant to be used, as follows:
59+
60+
```elixir
61+
defmacro plug(mod) do
62+
mod = Macro.expand_literals(mod, %{__CALLER__ | function: {:call, 2}})
63+
64+
quote do
65+
@plugs unquote(mod)
66+
end
67+
end
68+
```
69+
70+
In the example above, since `mod` is used only within the `call/2` function, we prematuraly expand module reference as if it was inside the `call/2` function. Now `MyApp.Authentication` is only a runtime dependency of `MyApp`, no longer a compile-time one.
71+
72+
Note, however, the above must only be done if your macros do not attempt to invoke any function, access any struct, or any other metadata of the module at compile-time. If you interact with the module given to a macro anywhere outside of definition of a function, then you effectively have a compile-time dependency. And, even though you generally want to avoid them, it is not always possible.
73+
74+
In actual projects, developers may use `mix xref trace path/to/file.ex` to execute a file and have it print information about which modules it depends on, and if those modules are compile-time, runtime, or export dependencies. See `mix xref` for more information.
75+
576
## Large code generation
677

778
#### Problem

0 commit comments

Comments
 (0)