You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: lib/elixir/pages/anti-patterns/code-anti-patterns.md
+40-51
Original file line number
Diff line number
Diff line change
@@ -309,9 +309,13 @@ There are few known exceptions to this anti-pattern:
309
309
310
310
#### Problem
311
311
312
-
In Elixir, it is possible to access values from `Map`s, which are key-value data structures, either statically or dynamically. When a key is expected to exist in the map, it must be accessed using the `map.key` notation, which asserts the key exists. If the key does not exist, an exception is raised (and in some situations also compiler warnings), allowing developers to catch bugs early on.
312
+
In Elixir, it is possible to access values from `Map`s, which are key-value data structures, either statically or dynamically.
313
313
314
-
`map[:key]` must be used with optional keys. This way, if the informed key does not exist, `nil` is returned. When used with required keys, this return can be confusing and allow `nil` values to pass through the system, while `map.key` would raise upfront. In this way, this anti-pattern may cause bugs in the code.
314
+
When a key is expected to exist in a map, it must be accessed using the `map.key` notation, making it clear to developers (and the compiler) that the key must exist. If the key does not exist, an exception is raised (and in some cases also compiler warnings). This is also known as the static notation, as the key is known at the time of writing the code.
315
+
316
+
When a key is optional, the `map[:key]` notation must be used instead. This way, if the informed key does not exist, `nil` is returned. This is the dynamic notation, as it also supports dynamic key access, such as `map[some_var]`.
317
+
318
+
When you use `map[:key]` to access a key that always exists in the map, you are making the code less clear for developers and for the compiler, as they now need to work with the assumption the key may not be there. This mismatch may also make it harder to track certain bugs. If the key is unexpected missing, you will have a `nil` value propagate through the system, instead of raising on map access.
315
319
316
320
#### Example
317
321
@@ -321,8 +325,6 @@ The function `plot/1` tries to draw a graphic to represent the position of a poi
321
325
defmoduleGraphicsdo
322
326
defplot(point) do
323
327
# Some other code...
324
-
325
-
# Dynamic access to use point values
326
328
{point[:x], point[:y], point[:z]}
327
329
end
328
330
end
@@ -331,92 +333,77 @@ end
331
333
```elixir
332
334
iex> point_2d = %{x:2, y:3}
333
335
%{x:2, y:3}
334
-
iex> point_3d = %{x:5, y:6, z:nil}
335
-
%{x:5, y:6, z:nil}
336
+
iex> point_3d = %{x:5, y:6, z:7}
337
+
%{x:5, y:6, z:7}
336
338
iex>Graphics.plot(point_2d)
337
-
{2, 3, nil}# <= ambiguous return
339
+
{2, 3, nil}
338
340
iex>Graphics.plot(point_3d)
339
-
{5, 6, nil}
341
+
{5, 6, 7}
342
+
```
343
+
344
+
Given we want to plot both 2D and 3D points, the behaviour above is expected. But what happens if we forget to pass a point with either `:x` or `:y`?
345
+
346
+
```elixir
347
+
iex> bad_point = %{y:3, z:4}
348
+
%{y:3, z:4}
349
+
iex>Graphics.plot(bad_point)
350
+
{nil, 3, 4}
340
351
```
341
352
342
-
As can be seen in the example above, even when the key `:z` does not exist in the map (`point_2d`), dynamic access returns the value `nil`. This return can be dangerous because of its ambiguity. It is not possible to conclude from it whether the map has the key `:z` or not. If the function relies on the return value to make decisions about how to plot a point, this can be problematic and even cause errors when testing the code.
353
+
The behaviour above is unexpected because our function should not work with points without a `:x` key. This leads to subtle bugs, as we may now pass `nil`to another function, instead of raising early on.
343
354
344
355
#### Refactoring
345
356
346
-
To remove this anti-pattern, whenever accessing an existing key of `Atom` type in the map, replace the dynamic `map[:key]` syntax by the static `map.key` notation. This way, when a non-existent key is accessed, Elixir raises an error immediately, allowing developers to find bugs faster. The next code illustrates the refactoring of `plot/1`, removing this anti-pattern:
357
+
To remove this anti-pattern, we must use the dynamic `map[:key]` syntax and the static `map.key` notation according to our requirements. We expect `:x` and `:y` to always exist, but not `:z`. The next code illustrates the refactoring of `plot/1`, removing this anti-pattern:
347
358
348
359
```elixir
349
360
defmoduleGraphicsdo
350
361
defplot(point) do
351
362
# Some other code...
352
-
353
-
# Strict access to use point values
354
-
{point.x, point.y, point.z}
363
+
{point.x, point.y, point[:z]}
355
364
end
356
365
end
357
366
```
358
367
359
368
```elixir
360
-
iex> point_2d = %{x:2, y:3}
361
-
%{x:2, y:3}
362
-
iex> point_3d = %{x:5, y:6, z:nil}
363
-
%{x:5, y:6, z:nil}
364
369
iex>Graphics.plot(point_2d)
365
-
** (KeyError) key :znot found in: %{x:2, y:3} # <= explicitly warns that
366
-
graphic.ex:6: Graphics.plot/1# <= the :z key does not exist!
367
-
iex>Graphics.plot(point_3d)
368
-
{5, 6, nil}
370
+
{2, 3, nil}
371
+
iex>Graphics.plot(bad_point)
372
+
** (KeyError) key :xnot found in: %{y:3, z:4} # <= explicitly warns that
373
+
graphic.ex:4: Graphics.plot/1# <= the :z key does not exist!
369
374
```
370
375
371
376
Overall, the usage of `map.key` and `map[:key]` encode important information about your data structure, allowing developers to be clear about their intent. See both `Map` and `Access` module documentation for more information and examples.
372
377
373
-
An even simpler alternative to refactor this anti-pattern is to use pattern matching:
378
+
An alternative to refactor this anti-pattern is to use pattern matching, defining explicit clauses for 2d vs 3d points:
374
379
375
380
```elixir
376
381
defmoduleGraphicsdo
377
-
defplot(%{x: x, y: y, z: z}) do
382
+
# 2d
383
+
defplot(%{x: x, y: y}) do
378
384
# Some other code...
385
+
{x, y}
386
+
end
379
387
380
-
# Strict access to use point values
388
+
# 3d
389
+
defplot(%{x: x, y: y, z: z}) do
390
+
# Some other code...
381
391
{x, y, z}
382
392
end
383
393
end
384
394
```
385
395
386
-
```elixir
387
-
iex> point_2d = %{x:2, y:3}
388
-
%{x:2, y:3}
389
-
iex> point_3d = %{x:5, y:6, z:nil}
390
-
%{x:5, y:6, z:nil}
391
-
iex>Graphics.plot(point_2d)
392
-
** (FunctionClauseError) no function clause matching inGraphics.plot/1
393
-
graphic.ex:2: Graphics.plot/1# <= the :z key does not exist!
394
-
iex>Graphics.plot(point_3d)
395
-
{5, 6, nil}
396
-
```
397
-
398
-
Pattern-matching is specially useful when matching over multiple keys at once and also when you want to match and assert on the values of a map.
396
+
Pattern-matching is specially useful when matching over multiple keys as well as on the values themselves at once.
399
397
400
-
Another alternative is to use structs. By default, structs only support static access to its fields:
398
+
Another option is to use structs. By default, structs only support static access to its fields. In such scenarios, you may consider defining structs for both 2D and 3D points:
401
399
402
400
```elixir
403
-
defmodulePoint.2Ddo
401
+
defmodulePoint2Ddo
404
402
@enforce_keys [:x, :y]
405
403
defstruct [x:nil, y:nil]
406
404
end
407
405
```
408
406
409
-
```elixir
410
-
iex> point = %Point.2D{x:2, y:3}
411
-
%Point.2D{x:2, y:3}
412
-
iex> point.x # <= strict access to use point values
413
-
2
414
-
iex> point.z # <= trying to access a non-existent key
415
-
** (KeyError) key :znot found in: %Point{x:2, y:3}
416
-
iex> point[:x] # <= by default, struct does not support dynamic access
417
-
** (UndefinedFunctionError) ... (Point does not implement the Access behaviour)
418
-
```
419
-
420
407
Generally speaking, structs are useful when sharing data structures across modules, at the cost of adding a compile time dependency between these modules. If module `A` uses a struct defined in module `B`, `A` must be recompiled if the fields in the struct `B` change.
421
408
422
409
#### Additional remarks
@@ -498,7 +485,7 @@ case some_function(arg) do
498
485
end
499
486
```
500
487
501
-
In particular, avoid matching solely on `_`, as shown below, as it is less clear in intent and it may hide bugs if `some_function/1` adds new return values in the future:
488
+
In particular, avoid matching solely on `_`, as shown below:
502
489
503
490
```elixir
504
491
casesome_function(arg) do
@@ -507,6 +494,8 @@ case some_function(arg) do
507
494
end
508
495
```
509
496
497
+
Matching on `_` is less clear in intent and it may hide bugs if `some_function/1` adds new return values in the future.
498
+
510
499
#### Additional remarks
511
500
512
501
This anti-pattern was formerly known as [Speculative assumptions](https://github.com/lucasvegi/Elixir-Code-Smells#speculative-assumptions).
Copy file name to clipboardExpand all lines: lib/elixir/pages/anti-patterns/design-anti-patterns.md
+2-63
Original file line number
Diff line number
Diff line change
@@ -1,7 +1,6 @@
1
1
# Design-related anti-patterns
2
2
3
-
This document outlines potential anti-patterns related to your modules, functions, and the role they
4
-
play within a codebase.
3
+
This document outlines potential anti-patterns related to your modules, functions, and the role they play within a codebase.
5
4
6
5
## Alternative return types
7
6
@@ -242,66 +241,6 @@ defmodule MyApp do
242
241
end
243
242
```
244
243
245
-
## Propagating invalid data
246
-
247
-
#### Problem
248
-
249
-
This anti-pattern refers to a function that does not validate its parameters and propagates them to other functions, which can produce internal unexpected behavior. When an error is raised inside a function due to an invalid parameter value, it can be confusing for developers and make it harder to locate and fix the error.
250
-
251
-
#### Example
252
-
253
-
An example of this anti-pattern is when a function receives an invalid parameter and then passes it to other functions, either in the same library or in a third-party library. This can cause an error to be raised deep inside the call stack, which may be confusing for the developer who is working with invalid data. As shown next, the function `foo/1` is a user-facing API which doesn't validate its parameters at the boundary. In this way, it is possible that invalid data will be passed through, causing an error that is obscure and hard to debug.
254
-
255
-
```elixir
256
-
defmoduleMyLibrarydo
257
-
deffoo(invalid_data) do
258
-
# Some other code...
259
-
260
-
MyLibrary.Internal.sum(1, invalid_data)
261
-
end
262
-
end
263
-
```
264
-
265
-
```elixir
266
-
iex>MyLibrary.foo(2)
267
-
3
268
-
iex>MyLibrary.foo("José") # With invalid data
269
-
** (ArithmeticError) bad argument in arithmetic expression:1+"José"
270
-
:erlang.+(1, "José")
271
-
my_library.ex:4: MyLibrary.Internal.sum/2
272
-
```
273
-
274
-
#### Refactoring
275
-
276
-
To remove this anti-pattern, the client code must validate input parameters at the boundary with the user, via guard clauses, pattern matching, or conditionals. This prevents errors from occurring elsewhere in the call stack, making them easier to understand and debug. This refactoring also allows libraries to be implemented without worrying about creating internal protection mechanisms. The next code snippet illustrates the refactoring of `foo/1`, removing this anti-pattern:
277
-
278
-
```elixir
279
-
defmoduleMyLibrarydo
280
-
deffoo(data) whenis_integer(data) do
281
-
# Some other code
282
-
283
-
MyLibrary.Internal.sum(1, data)
284
-
end
285
-
end
286
-
```
287
-
288
-
```elixir
289
-
iex>MyLibrary.foo(2) # With valid data
290
-
3
291
-
iex>MyLibrary.foo("José") # With invalid data
292
-
** (FunctionClauseError) no function clause matching inMyLibrary.foo/1.
293
-
The following arguments were given to MyLibrary.foo/1:
294
-
295
-
# 1
296
-
"José"
297
-
298
-
my_library.ex:2: MyLibrary.foo/1
299
-
```
300
-
301
-
#### Additional remarks
302
-
303
-
This anti-pattern was formerly known as [Working with invalid data](https://github.com/lucasvegi/Elixir-Code-Smells#working-with-invalid-data).
304
-
305
244
## Unrelated multi-clause function
306
245
307
246
#### Problem
@@ -377,7 +316,7 @@ end
377
316
378
317
You can see this pattern in practice within Elixir itself. The `+/2` operator can add `Integer`s and `Float`s together, but not `String`s, which instead use the `<>/2` operator. In this sense, it is reasonable to handle integers and floats in the same operation, but strings are unrelated enough to deserve their own function.
379
318
380
-
You will also find examples in Elixir of functions that work with any struct, such as `struct/2`:
319
+
You will also find examples in Elixir of functions that work with any struct, which would seemingly be an occurrence of this anti-pattern, such as `struct/2`:
Copy file name to clipboardExpand all lines: lib/elixir/pages/anti-patterns/macro-anti-patterns.md
+1-1
Original file line number
Diff line number
Diff line change
@@ -63,7 +63,7 @@ end
63
63
64
64
#### Problem
65
65
66
-
**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.
66
+
*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.
Copy file name to clipboardExpand all lines: lib/elixir/pages/anti-patterns/process-anti-patterns.md
+18-11
Original file line number
Diff line number
Diff line change
@@ -6,7 +6,7 @@ This document outlines potential anti-patterns related to processes and process-
6
6
7
7
#### Problem
8
8
9
-
This anti-pattern refers to code that is unnecessarily organized by processes. A process itself does not represent an anti-pattern, but it should only be used to model runtime properties (such as concurrency, access to shared resources, and event scheduling). When you use a process for code organization, it can create bottlenecks in the system.
9
+
This anti-pattern refers to code that is unnecessarily organized by processes. A process itself does not represent an anti-pattern, but it should only be used to model runtime properties (such as concurrency, access to shared resources, error isolation, etc). When you use a process for code organization, it can create bottlenecks in the system.
10
10
11
11
#### Example
12
12
@@ -261,7 +261,9 @@ defmodule Counter do
261
261
useGenServer
262
262
263
263
@doc "Starts a counter process."
264
-
defstart(initial_value, name \\__MODULE__) whenis_integer(initial_value) do
0 commit comments