Skip to content

Commit 36e7105

Browse files
authored
Remaining macro-anti-patterns (Anti-pattern documentation) (#12769)
1 parent e00524b commit 36e7105

File tree

1 file changed

+97
-2
lines changed

1 file changed

+97
-2
lines changed

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

+97-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,106 @@ This document outlines anti-patterns related to meta-programming.
44

55
## Unnecessary macros
66

7-
TODO.
7+
#### Problem
8+
9+
**Macros** are powerful meta-programming mechanisms that can be used in Elixir to extend the language. While using macros is not an anti-pattern in itself, this meta-programming mechanism should only be used when absolutely necessary. Whenever a macro is used, but it would have been possible to solve the same problem using functions or other existing Elixir structures, the code becomes unnecessarily more complex and less readable. Because macros are more difficult to implement and reason about, their indiscriminate use can compromise the evolution of a system, reducing its maintainability.
10+
11+
#### Example
12+
13+
The `MyMath` module implements the `sum/2` macro to perform the sum of two numbers received as parameters. While this code has no syntax errors and can be executed correctly to get the desired result, it is unnecessarily more complex. By implementing this functionality as a macro rather than a conventional function, the code became less clear:
14+
15+
```elixir
16+
defmodule MyMath do
17+
defmacro sum(v1, v2) do
18+
quote do
19+
unquote(v1) + unquote(v2)
20+
end
21+
end
22+
end
23+
```
24+
```elixir
25+
iex> require MyMath
26+
MyMath
27+
iex> MyMath.sum(3, 5)
28+
8
29+
iex> MyMath.sum(3 + 1, 5 + 6)
30+
15
31+
```
32+
33+
#### Refactoring
34+
35+
To remove this anti-pattern, the developer must replace the unnecessary macro with structures that are simpler to write and understand, such as named functions. The code shown below is the result of the refactoring of the previous example. Basically, the `sum/2` macro has been transformed into a conventional named function. Note that the `require/2` call is no longer needed:
36+
37+
```elixir
38+
defmodule MyMath do
39+
def sum(v1, v2) do # <= The macro became a named function
40+
v1 + v2
41+
end
42+
end
43+
```
44+
```elixir
45+
iex> MyMath.sum(3, 5)
46+
8
47+
iex> MyMath.sum(3+1, 5+6)
48+
15
49+
```
850

951
## Large code generation by macros
1052

11-
TODO.
53+
#### Problem
54+
55+
This anti-pattern is related to macros that generate too much code. When a macro generates a large amount of code, it impacts how the compiler and/or the runtime work. The reason for this is that Elixir may have to expand, compile, and execute the code multiple times, which will make compilation slower and the resulting compiled artifacts larger.
56+
57+
#### Example
58+
59+
Imagine you are defining a router for a web application, where you could have macros like `get/2`. On every invocation of the macro (which could be hundreds), the code inside `get/2` will be expanded and compiled, which can generate a large volume of code overall.
60+
61+
```elixir
62+
defmodule Routes do
63+
defmacro get(route, handler) do
64+
quote do
65+
route = unquote(route)
66+
handler = unquote(handler)
67+
68+
if not is_binary(route) do
69+
raise ArgumentError, "route must be a binary"
70+
end
71+
72+
if not is_atom(handler) do
73+
raise ArgumentError, "route must be a module"
74+
end
75+
76+
@store_route_for_compilation {route, handler}
77+
end
78+
end
79+
end
80+
```
81+
82+
#### Refactoring
83+
84+
To remove this anti-pattern, the developer should simplify the macro, delegating part of its work to other functions. As shown below, by encapsulating the code inside `quote/1` inside the function `__define__/3` instead, we reduce the code that is expanded and compiled on every invocation of the macro, and instead we dispatch to a function to do the bulk of the work.
85+
86+
```elixir
87+
defmodule Routes do
88+
defmacro get(route, handler) do
89+
quote do
90+
Routes.__define__(__MODULE__, unquote(route), unquote(handler))
91+
end
92+
end
93+
94+
def __define__(module, route, handler) do
95+
if not is_binary(route) do
96+
raise ArgumentError, "route must be a binary"
97+
end
98+
99+
if not is_atom(handler) do
100+
raise ArgumentError, "route must be a module"
101+
end
102+
103+
Module.put_attribute(module, :store_route_for_compilation, {route, handler})
104+
end
105+
end
106+
```
12107

13108
## `use` instead of `import`
14109

0 commit comments

Comments
 (0)