Skip to content

Commit 30d1968

Browse files
authored
Enhance non-assertive map access anti-pattern documentation (#14501)
1 parent 81f885d commit 30d1968

File tree

1 file changed

+49
-4
lines changed

1 file changed

+49
-4
lines changed

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

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,13 @@ When a key is optional, the `map[:key]` notation must be used instead. This way,
324324

325325
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.
326326

327+
##### Table: Comparison of map access notations
328+
329+
| Access notation | Key exists | Key doesn't exist | Use case |
330+
| --------------- | ---------- | ----------------- | -------- |
331+
| `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+
327334
#### Example
328335

329336
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:
@@ -357,7 +364,19 @@ iex> Graphics.plot(bad_point)
357364
{nil, 3, 4}
358365
```
359366

360-
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.
361380

362381
#### Refactoring
363382

@@ -380,9 +399,15 @@ iex> Graphics.plot(bad_point)
380399
graphic.ex:4: Graphics.plot/1
381400
```
382401

383-
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
384407

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:
386411

387412
```elixir
388413
defmodule Graphics do
@@ -400,7 +425,19 @@ defmodule Graphics do
400425
end
401426
```
402427

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 in Graphics.plot/1
435+
436+
The following arguments were given to Graphics.plot/1:
437+
438+
# 1
439+
%{x: 5}
440+
```
404441

405442
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:
406443

@@ -413,6 +450,14 @@ end
413450

414451
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.
415452

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+
416461
#### Additional remarks
417462

418463
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

Comments
 (0)