Skip to content

Commit e05bfc7

Browse files
author
José Valim
committed
Improve docs and error handling for Access
Signed-off-by: José Valim <[email protected]>
1 parent cb43070 commit e05bfc7

File tree

3 files changed

+103
-8
lines changed

3 files changed

+103
-8
lines changed

lib/elixir/lib/access.ex

Lines changed: 83 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
defmodule 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

lib/elixir/lib/exception.ex

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,6 +640,10 @@ defmodule UndefinedFunctionError do
640640
"undefined function " <> Exception.format_mfa(module, function, arity) <>
641641
" (function #{fa} is not available)"
642642
end
643+
644+
def message(%{reason: reason, module: module, function: function, arity: arity}) do
645+
"undefined function " <> Exception.format_mfa(module, function, arity) <> " (#{reason})"
646+
end
643647
end
644648

645649
defmodule FunctionClauseError do

lib/elixir/test/elixir/access_test.exs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,20 @@ defmodule AccessTest do
5959
assert Access.get_and_update(%{}, :foo, fn nil -> {:ok, :baz} end) == {:ok, %{foo: :baz}}
6060
assert Access.get_and_update(%{foo: :bar}, :foo, fn :bar -> {:ok, :baz} end) == {:ok, %{foo: :baz}}
6161
end
62+
63+
test "for struct" do
64+
defmodule Sample do
65+
defstruct [:name]
66+
end
67+
68+
assert_raise UndefinedFunctionError,
69+
"undefined function AccessTest.Sample.fetch/2 (AccessTest.Sample does not implement the Access behaviour)", fn ->
70+
Access.fetch(struct(Sample, []), :name)
71+
end
72+
73+
assert_raise UndefinedFunctionError,
74+
"undefined function AccessTest.Sample.get_and_update/3 (AccessTest.Sample does not implement the Access behaviour)", fn ->
75+
Access.get_and_update(struct(Sample, []), :name, fn nil -> {:ok, :baz} end)
76+
end
77+
end
6278
end

0 commit comments

Comments
 (0)