1
1
defmodule Access do
2
2
@ moduledoc """
3
- Dictionary-like access to data structures via the `foo[bar]` syntax.
3
+ Key-based access to data structures via the `foo[bar]` syntax.
4
4
5
- This module also empowers `Kernel`s nested update functions
6
- `Kernel.get_in/2`, `Kernel.put_in/3`, `Kernel.update_in/3` and
7
- `Kernel.get_and_update_in/3`.
5
+ Elixir provides two syntaxes for accessing values. `user[:name]`
6
+ is used by dynamic structures, like maps and keywords, while
7
+ `user.name` is used by structs. The main difference is that
8
+ `user[:name]` won't raise if the key `:name` is missing but
9
+ `user.name` will raise if there is no `:name` key.
8
10
9
- ## Examples
11
+ ## Key-based lookups
10
12
11
- Out of the box, Access works with built-in dictionaries: `Keyword`
12
- and `Map`:
13
+ Out of the box, Access works with `Keyword` and `Map`:
13
14
14
15
iex> keywords = [a: 1, b: 2]
15
16
iex> keywords[:a]
@@ -23,13 +24,67 @@ defmodule Access do
23
24
iex> star_ratings[1.5]
24
25
"★☆"
25
26
27
+ Access can be combined with `Kernel.put_in/3` to put a value
28
+ in a given key:
29
+
30
+ iex> map = %{a: 1, b: 2}
31
+ iex> put_in map[:a], 3
32
+ %{a: 3, b: 2}
33
+
34
+ This syntax is very convenient as it can be nested arbitrarily:
35
+
36
+ iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
37
+ iex> put_in users["john"][:age], 28
38
+ %{"john" => %{age: 28}, "meg" => %{age: 23}}
39
+
26
40
Furthermore, Access transparently ignores `nil` values:
27
41
28
42
iex> keywords = [a: 1, b: 2]
29
43
iex> keywords[:c][:unknown]
30
44
nil
31
45
32
- The key comparison must be implemented using the `===` operator.
46
+ Since Access is a behaviour, it can be implemented to key-value
47
+ data structures. Access requires the key comparison to be
48
+ implemented using the `===` operator.
49
+
50
+ ## Field-based lookups
51
+
52
+ The Access syntax (`foo[bar]`) cannot be used to access fields in
53
+ structs. That's by design, as Access is meant to be used for
54
+ dynamic key-value structures, like maps and keywords, and not
55
+ by static ones like structs.
56
+
57
+ However Elixir already provides a field-based lookup for structs.
58
+ Imagine a struct named `User` with name and age fields. The
59
+ following would raise:
60
+
61
+ user = %User{name: "john"}
62
+ user[:name]
63
+ ** (UndefinedFunctionError) undefined function User.fetch/2
64
+ (User does not implement the Access behaviour)
65
+
66
+ Structs instead use the `user.name` syntax:
67
+
68
+ user.name
69
+ #=> "john"
70
+
71
+ The same `user.name` syntax can also be used by `Kernel.put_in/2`
72
+ to for updating structs fields:
73
+
74
+ put_in user.name, "mary"
75
+ %User{name: "mary"}
76
+
77
+ Differently from `user[:name]`, `user.name` cannot be extended by
78
+ the developers, and will be always restricted to only maps and
79
+ structs.
80
+
81
+ Summing up:
82
+
83
+ * `user[:name]` is used by dynamic structures, is extensible and
84
+ does not raise on missing keys
85
+ * `user.name` is used by static structures, it is not extensible
86
+ and it will raise on missing keys
87
+
33
88
"""
34
89
35
90
@ type t :: list | map | nil
@@ -39,6 +94,20 @@ defmodule Access do
39
94
@ callback fetch ( t , key ) :: { :ok , value } | :error
40
95
@ callback get_and_update ( t , key , ( value -> { value , value } ) ) :: { value , t }
41
96
97
+ defmacrop raise_undefined_behaviour ( e , struct , top ) do
98
+ quote do
99
+ stacktrace = System . stacktrace
100
+ e =
101
+ case stacktrace do
102
+ [ unquote ( top ) | _ ] ->
103
+ % { unquote ( e ) | reason: "#{ inspect unquote ( struct ) } does not implement the Access behaviour" }
104
+ _ ->
105
+ unquote ( e )
106
+ end
107
+ reraise e , stacktrace
108
+ end
109
+ end
110
+
42
111
@ doc """
43
112
Fetches the container's value for the given key.
44
113
"""
@@ -47,6 +116,9 @@ defmodule Access do
47
116
48
117
def fetch ( % { __struct__: struct } = container , key ) do
49
118
struct . fetch ( container , key )
119
+ rescue
120
+ e in UndefinedFunctionError ->
121
+ raise_undefined_behaviour e , struct , { ^ struct , :fetch , [ ^ container , ^ key ] , _ }
50
122
end
51
123
52
124
def fetch ( % { } = map , key ) do
@@ -96,6 +168,9 @@ defmodule Access do
96
168
97
169
def get_and_update ( % { __struct__: struct } = container , key , fun ) do
98
170
struct . get_and_update ( container , key , fun )
171
+ rescue
172
+ e in UndefinedFunctionError ->
173
+ raise_undefined_behaviour e , struct , { ^ struct , :get_and_update , [ ^ container , ^ key , ^ fun ] , _ }
99
174
end
100
175
101
176
def get_and_update ( % { } = map , key , fun ) do
0 commit comments