|
2 | 2 |
|
3 | 3 | This document outlines potential anti-patterns related to meta-programming.
|
4 | 4 |
|
| 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 | + |
5 | 76 | ## Large code generation
|
6 | 77 |
|
7 | 78 | #### Problem
|
|
0 commit comments