Skip to content

Commit 7a1ebc5

Browse files
committed
anti-patterns: add "Transitive dependencies" to Meta-programming anti-patterns
1 parent c7eb3e5 commit 7a1ebc5

File tree

1 file changed

+109
-0
lines changed

1 file changed

+109
-0
lines changed

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

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,115 @@ iex> MyMath.sum(3+1, 5+6)
107107
15
108108
```
109109

110+
## Transitive dependencies
111+
112+
#### Context
113+
114+
This anti-pattern is related to dependencies between files in Elixir. Elixir tracks
115+
three different types of dependencies between compiled files to know when a file needs
116+
to be recompiled. The different dependency types are explained in the documentation of
117+
[`mix xref`](https://hexdocs.pm/mix/Mix.Tasks.Xref.html#module-dependency-types).
118+
119+
#### Problem
120+
121+
Macro-exposing modules are bound to be compile-time dependencies of the modules using
122+
their macros. If they depend on other modules from within the same project, then they
123+
become the source of compile-connected dependencies. This means that if the file
124+
containing the module they depend on is modified, all files depending on this module will
125+
need to be recompiled.
126+
127+
This becomes especially problematic when projects grow and the number of compile-connected
128+
dependencies is not kept under control, making modifying any file trigger a recompilation
129+
of many other files and slowing down the development process.
130+
131+
Note that depending on modules from external libraries is not a problem, as these modules
132+
are only compiled once when the library is compiled.
133+
134+
#### Example
135+
136+
The code below defines three modules `ModuleA`, `Macros` and `MacroUtils`, each in their
137+
own file. The dependencies between the three files are as follows:
138+
139+
- `lib/module_a.ex` has a _compile_ dependency on `lib/macros.ex` because `ModuleA`
140+
calls a macro defined in `Macros`,
141+
- `lib/macros.ex` has a _runtime_ dependency on `lib/macro_utils.ex` because `Macros`
142+
calls a function defined in `MacroUtils` within the body of its macro.
143+
144+
In this example, `lib/macro_utils.ex` is a compile dependency of `lib/module_a.ex`,
145+
through a transitive dependency via `lib/macros.ex`. This means that modifying
146+
`lib/macro_utils.ex` will trigger a recompilation of `lib/module_a.ex`, as if
147+
`lib/module_a.ex` had a direct compile dependency on `lib/macro_utils.ex`.
148+
149+
Note that if `MacroUtils` itself depended on other modules directly or indirectly,
150+
modifying the files containing these modules would also trigger a recompilation of
151+
`lib/module_a.ex`.
152+
153+
```elixir
154+
# lib/module_a.ex
155+
defmodule ModuleA do
156+
require Macros
157+
158+
def hello do
159+
Macros.greet()
160+
end
161+
end
162+
```
163+
164+
```elixir
165+
# lib/macros.ex
166+
defmodule Macros do
167+
defmacro greet do
168+
module = MacroUtils.get_module(__CALLER__)
169+
170+
quote do
171+
IO.puts("Hello from #{unquote(module)}")
172+
end
173+
end
174+
end
175+
```
176+
177+
```elixir
178+
# lib/macro_utils.ex
179+
defmodule MacroUtils do
180+
def get_module(caller) do
181+
caller.module
182+
end
183+
end
184+
```
185+
186+
#### Refactoring
187+
188+
To remove this pattern, the developer must remove any dependency to other modules from
189+
the same project. In the code shown below, the `MacroUtils` module was deleted and the
190+
`get_module/1` function was moved to the `Macros` module. This way, the `Macros` module
191+
no longer depends on any other module.
192+
193+
```elixir
194+
# lib/macros.ex
195+
defmodule Macros do
196+
defmacro greet do
197+
module = get_module(__CALLER__)
198+
199+
quote do
200+
IO.puts("Hello from #{unquote(module)}")
201+
end
202+
end
203+
204+
defp get_module(caller) do
205+
caller.module
206+
end
207+
end
208+
```
209+
210+
#### Additional remarks
211+
212+
Whilst this guide only covers macros, modules can be compile dependencies of other modules
213+
in other ways. For example, calling another module in the module, in a module attribute
214+
for example, will also create a compile dependency. Some external libraries may also
215+
create compile dependencies by using macros.
216+
[`mix xref`](https://hexdocs.pm/mix/main/Mix.Tasks.Xref.html#module-a-brief-introduction-to-xref)
217+
can be useful to identify these dependencies and refactor the code to remove them.
218+
110219
## `use` instead of `import`
111220

112221
#### Problem

0 commit comments

Comments
 (0)