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
+49-4Lines changed: 49 additions & 4 deletions
Original file line number
Diff line number
Diff line change
@@ -324,6 +324,13 @@ When a key is optional, the `map[:key]` notation must be used instead. This way,
324
324
325
325
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 unexpectedly missing, you will have a `nil` value propagate through the system, instead of raising on map access.
326
326
327
+
##### Table: Comparison of map access notations
328
+
329
+
| Access notation | Key exists | Key doesn't exist | Use case |
|`map.key`| Returns the value | Raises `KeyError`| Structs and maps with known atom keys |
332
+
|`map[:key]`| Returns the value | Returns `nil`| Any `Access`-based data structure, optional keys |
333
+
327
334
#### Example
328
335
329
336
The function `plot/1` tries to draw a graphic to represent the position of a point in a Cartesian plane. This function receives a parameter of `Map` type with the point attributes, which can be a point of a 2D or 3D Cartesian coordinate system. This function uses dynamic access to retrieve values for the map keys:
The behavior 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.
367
+
The behavior 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, as shown next:
368
+
369
+
```iex
370
+
iex> point_without_x = %{y: 10}
371
+
%{y: 10}
372
+
iex> {x, y, _} = Graphics.plot(point_without_x)
373
+
{nil, 10, nil}
374
+
iex> distance_from_origin = :math.sqrt(x * x + y * y)
375
+
** (ArithmeticError) bad argument in arithmetic expression
376
+
:erlang.*(nil, nil)
377
+
```
378
+
379
+
The error above occurs later in the code because `nil` (from missing `:x`) is invalid for arithmetic operations, making it harder to identify the original issue.
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.
402
+
This is beneficial because:
403
+
404
+
1. It makes your expectations clear to others reading the code
405
+
2. It fails fast when required data is missing
406
+
3. It allows the compiler to provide warnings when accessing non-existent fields, particularly in compile-time structures like structs
384
407
385
-
An alternative to refactor this anti-pattern is to use pattern matching, defining explicit clauses for 2d vs 3d points:
408
+
Overall, the usage of `map.key` and `map[:key]` encode important information about your data structure, allowing developers to be clear about their intent. The `Access` module documentation also provides useful reference on this topic. You can also consider the `Map` module when working with maps of any keys, which contains functions for fetching keys (with or without default values), updating and removing keys, traversals, and more.
409
+
410
+
An alternative to refactor this anti-pattern is to use pattern matching, defining explicit clauses for 2D vs 3D points:
386
411
387
412
```elixir
388
413
defmoduleGraphicsdo
@@ -400,7 +425,19 @@ defmodule Graphics do
400
425
end
401
426
```
402
427
403
-
Pattern-matching is specially useful when matching over multiple keys as well as on the values themselves at once.
428
+
Pattern-matching is specially useful when matching over multiple keys as well as on the values themselves at once. In the example above, the code will not only extract the values but also verify that the required keys exist. If we try to call `plot/1` with a map that doesn't have the required keys, we'll get a `FunctionClauseError`:
429
+
430
+
```elixir
431
+
iex> incomplete_point = %{x:5}
432
+
%{x:5}
433
+
iex>Graphics.plot(incomplete_point)
434
+
** (FunctionClauseError) no function clause matching inGraphics.plot/1
435
+
436
+
The following arguments were given to Graphics.plot/1:
437
+
438
+
# 1
439
+
%{x:5}
440
+
```
404
441
405
442
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:
406
443
@@ -413,6 +450,14 @@ end
413
450
414
451
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.
415
452
453
+
In summary, Elixir provides several ways to access map values, each with different behaviors:
454
+
455
+
1.**Static access** (`map.key`): Fails fast when keys are missing, ideal for structs and maps with known atom keys
456
+
2.**Dynamic access** (`map[:key]`): Works with any `Access` data structure, suitable for optional fields, returns nil for missing keys
457
+
3.**Pattern matching**: Provides a powerful way to both extract values and ensure required map/struct keys exist in one operation
458
+
459
+
Choosing the right approach depends if the keys are known upfront or not. Static access and pattern matching are mostly equivalent (although pattern matching allows you to match on multiple keys at once, including matching on the struct name).
460
+
416
461
#### Additional remarks
417
462
418
463
This anti-pattern was formerly known as [Accessing non-existent map/struct fields](https://github.com/lucasvegi/Elixir-Code-Smells#accessing-non-existent-mapstruct-fields).
0 commit comments