11defmodule Access do
22 @ 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.
44
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.
810
9- ## Examples
11+ ## Key-based lookups
1012
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`:
1314
1415 iex> keywords = [a: 1, b: 2]
1516 iex> keywords[:a]
@@ -23,13 +24,67 @@ defmodule Access do
2324 iex> star_ratings[1.5]
2425 "★☆"
2526
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+
2640 Furthermore, Access transparently ignores `nil` values:
2741
2842 iex> keywords = [a: 1, b: 2]
2943 iex> keywords[:c][:unknown]
3044 nil
3145
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+
3388 """
3489
3590 @ type t :: list | map | nil
@@ -39,6 +94,20 @@ defmodule Access do
3994 @ callback fetch ( t , key ) :: { :ok , value } | :error
4095 @ callback get_and_update ( t , key , ( value -> { value , value } ) ) :: { value , t }
4196
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+
42111 @ doc """
43112 Fetches the container's value for the given key.
44113 """
@@ -47,6 +116,9 @@ defmodule Access do
47116
48117 def fetch ( % { __struct__: struct } = container , key ) do
49118 struct . fetch ( container , key )
119+ rescue
120+ e in UndefinedFunctionError ->
121+ raise_undefined_behaviour e , struct , { ^ struct , :fetch , [ ^ container , ^ key ] , _ }
50122 end
51123
52124 def fetch ( % { } = map , key ) do
@@ -96,6 +168,9 @@ defmodule Access do
96168
97169 def get_and_update ( % { __struct__: struct } = container , key , fun ) do
98170 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 ] , _ }
99174 end
100175
101176 def get_and_update ( % { } = map , key , fun ) do
0 commit comments