-
Notifications
You must be signed in to change notification settings - Fork 3.4k
docs: enhance non-assertive map access anti-pattern documentation #14501
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
josevalim
merged 6 commits into
elixir-lang:main
from
abreujp:improve-non-assertive-map-access-doc
May 20, 2025
Merged
Changes from 4 commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
66762a7
docs: enhance non-assertive map access anti-pattern documentation
abreujp e4ef581
docs: enhance map access table and examples
abreujp d4084b5
Update code-anti-patterns.md
josevalim a97338a
Apply suggestions from code review
josevalim fd56a8c
Update lib/elixir/pages/anti-patterns/code-anti-patterns.md
josevalim ed44b24
Update lib/elixir/pages/anti-patterns/code-anti-patterns.md
josevalim File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -324,6 +324,13 @@ | |
|
||
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. | ||
|
||
*Table: Comparison of Map Access Notations* | ||
Check failure on line 327 in lib/elixir/pages/anti-patterns/code-anti-patterns.md
|
||
|
||
| Access notation | Key exists | Key doesn't exist | Use case | | ||
| --------------- | ---------- | ----------------- | -------- | | ||
| `map.key` | Returns the value | Raises `KeyError` | Structs and maps with known atom keys | | ||
| `map[:key]` | Returns the value | Returns `nil` | Any `Access`-based data structure, optional keys | | ||
|
||
#### Example | ||
|
||
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 @@ | |
{nil, 3, 4} | ||
``` | ||
|
||
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. | ||
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: | ||
|
||
```iex | ||
iex> point_without_x = %{y: 10} | ||
%{y: 10} | ||
iex> {x, y, _} = Graphics.plot(point_without_x) | ||
{nil, 10, nil} | ||
iex> distance_from_origin = :math.sqrt(x * x + y * y) | ||
** (ArithmeticError) bad argument in arithmetic expression | ||
:erlang.*(nil, nil) | ||
abreujp marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
|
||
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. | ||
|
||
#### Refactoring | ||
|
||
|
@@ -380,9 +399,15 @@ | |
graphic.ex:4: Graphics.plot/1 | ||
``` | ||
|
||
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. | ||
This is beneficial because: | ||
|
||
1. It makes your expectations clear to others reading the code | ||
2. It fails fast when required data is missing | ||
3. It allows the compiler to provide warnings when accessing non-existent fields, particularly in compile-time structures like structs | ||
|
||
An alternative to refactor this anti-pattern is to use pattern matching, defining explicit clauses for 2d vs 3d points: | ||
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. | ||
|
||
An alternative to refactor this anti-pattern is to use pattern matching, defining explicit clauses for 2D vs 3D points: | ||
|
||
```elixir | ||
defmodule Graphics do | ||
|
@@ -400,7 +425,19 @@ | |
end | ||
``` | ||
|
||
Pattern-matching is specially useful when matching over multiple keys as well as on the values themselves at once. | ||
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`: | ||
|
||
```elixir | ||
iex> incomplete_point = %{x: 5} | ||
%{x: 5} | ||
iex> Graphics.plot(incomplete_point) | ||
** (FunctionClauseError) no function clause matching in Graphics.plot/1 | ||
|
||
The following arguments were given to Graphics.plot/1: | ||
|
||
# 1 | ||
%{x: 5} | ||
``` | ||
|
||
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: | ||
|
||
|
@@ -413,6 +450,14 @@ | |
|
||
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. | ||
|
||
In summary, Elixir provides several ways to access map values, each with different behaviors: | ||
|
||
1. **Static access** (`map.key`): Fails fast when keys are missing, ideal for structs and maps with known atom keys | ||
2. **Dynamic access** (`map[:key]`): Works with any `Access` data structure, suitable for optional fields, returns nil for missing keys | ||
3. **Pattern matching**: Provides a powerful way to both extract values and ensure required map/struct keys exist in one operation | ||
|
||
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). | ||
|
||
#### Additional remarks | ||
|
||
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). | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.