Skip to content

Commit e0c016c

Browse files
authored
Add Access.values/0 (#14350)
1 parent 5b0ed0d commit e0c016c

File tree

2 files changed

+151
-0
lines changed

2 files changed

+151
-0
lines changed

lib/elixir/lib/access.ex

+87
Original file line numberDiff line numberDiff line change
@@ -999,6 +999,93 @@ defmodule Access do
999999
raise ArgumentError, "Access.slice/1 expected a list, got: #{inspect(data)}"
10001000
end
10011001

1002+
@doc """
1003+
Returns a function that accesses all values in a map or a keyword list.
1004+
1005+
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
1006+
`Kernel.get_and_update_in/3`, and friends.
1007+
1008+
## Examples
1009+
1010+
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
1011+
iex> get_in(users, [Access.values(), :age]) |> Enum.sort()
1012+
[23, 27]
1013+
iex> update_in(users, [Access.values(), :age], fn age -> age + 1 end)
1014+
%{"john" => %{age: 28}, "meg" => %{age: 24}}
1015+
iex> put_in(users, [Access.values(), :planet], "Earth")
1016+
%{"john" => %{age: 27, planet: "Earth"}, "meg" => %{age: 23, planet: "Earth"}}
1017+
1018+
Values in keyword lists can be accessed as well:
1019+
1020+
iex> users = [john: %{age: 27}, meg: %{age: 23}]
1021+
iex> get_and_update_in(users, [Access.values(), :age], fn age -> {age, age + 1} end)
1022+
{[27, 23], [john: %{age: 28}, meg: %{age: 24}]}
1023+
1024+
By returning `:pop` from an accessor function, you can remove the accessed key and value
1025+
from the map or keyword list:
1026+
1027+
iex> require Integer
1028+
iex> numbers = [one: 1, two: 2, three: 3, four: 4]
1029+
iex> get_and_update_in(numbers, [Access.values()], fn num ->
1030+
...> if Integer.is_even(num), do: :pop, else: {num, to_string(num)}
1031+
...> end)
1032+
{[1, 2, 3, 4], [one: "1", three: "3"]}
1033+
1034+
An error is raised if the accessed structure is not a map nor a keyword list:
1035+
1036+
iex> get_in([1, 2, 3], [Access.values()])
1037+
** (RuntimeError) Access.values/0 expected a map or a keyword list, got: [1, 2, 3]
1038+
"""
1039+
@doc since: "1.19.0"
1040+
@spec values() :: Access.access_fun(data :: map() | keyword(), current_value :: list())
1041+
def values do
1042+
&values/3
1043+
end
1044+
1045+
defp values(:get, data = %{}, next) do
1046+
Enum.map(data, fn {_key, value} -> next.(value) end)
1047+
end
1048+
1049+
defp values(:get_and_update, data = %{}, next) do
1050+
{reverse_gets, updated_data} =
1051+
Enum.reduce(data, {[], %{}}, fn {key, value}, {gets, data_acc} ->
1052+
case next.(value) do
1053+
{get, update} -> {[get | gets], Map.put(data_acc, key, update)}
1054+
:pop -> {[value | gets], data_acc}
1055+
end
1056+
end)
1057+
1058+
{Enum.reverse(reverse_gets), updated_data}
1059+
end
1060+
1061+
defp values(op, data = [], next) do
1062+
values_keyword(op, data, next)
1063+
end
1064+
1065+
defp values(op, data = [{key, _value} | _tail], next) when is_atom(key) do
1066+
values_keyword(op, data, next)
1067+
end
1068+
1069+
defp values(_op, data, _next) do
1070+
raise "Access.values/0 expected a map or a keyword list, got: #{inspect(data)}"
1071+
end
1072+
1073+
defp values_keyword(:get, data, next) do
1074+
Enum.map(data, fn {key, value} when is_atom(key) -> next.(value) end)
1075+
end
1076+
1077+
defp values_keyword(:get_and_update, data, next) do
1078+
{reverse_gets, reverse_updated_data} =
1079+
Enum.reduce(data, {[], []}, fn {key, value}, {gets, data_acc} when is_atom(key) ->
1080+
case next.(value) do
1081+
{get, update} -> {[get | gets], [{key, update} | data_acc]}
1082+
:pop -> {[value | gets], data_acc}
1083+
end
1084+
end)
1085+
1086+
{Enum.reverse(reverse_gets), Enum.reverse(reverse_updated_data)}
1087+
end
1088+
10021089
defp normalize_range(%Range{first: first, last: last, step: step}, list)
10031090
when first < 0 or last < 0 do
10041091
count = length(list)

lib/elixir/test/elixir/access_test.exs

+64
Original file line numberDiff line numberDiff line change
@@ -292,4 +292,68 @@ defmodule AccessTest do
292292
assert get_in(input, [:list, Access.at!(0), :greeting]) == "hi"
293293
end
294294
end
295+
296+
describe "values/0" do
297+
@test_map %{a: 1, b: 2, c: 3, d: 4}
298+
@test_list [a: 1, b: 2, c: 3, d: 4]
299+
@error_msg_pattern ~r[^Access.values/0 expected a map or a keyword list, got: .*]
300+
301+
test "retrieves values in a map" do
302+
assert [1, 2, 3, 4] = get_in(@test_map, [Access.values()]) |> Enum.sort()
303+
end
304+
305+
test "retrieves values in a keyword list" do
306+
assert [1, 2, 3, 4] = get_in(@test_list, [Access.values()])
307+
end
308+
309+
test "gets and updates values in a map" do
310+
assert {gets, %{a: 3, b: 4, c: 5, d: 6}} =
311+
get_and_update_in(@test_map, [Access.values()], fn n -> {n + 1, n + 2} end)
312+
313+
assert [2, 3, 4, 5] = Enum.sort(gets)
314+
end
315+
316+
test "gets and updates values in a keyword list" do
317+
assert {[2, 3, 4, 5], [a: 3, b: 4, c: 5, d: 6]} =
318+
get_and_update_in(@test_list, [Access.values()], fn n -> {n + 1, n + 2} end)
319+
end
320+
321+
test "pops values from a map" do
322+
assert {gets, %{c: 4, d: 5}} =
323+
get_and_update_in(@test_map, [Access.values()], fn n ->
324+
if(n > 2, do: {-n, n + 1}, else: :pop)
325+
end)
326+
327+
assert [-4, -3, 1, 2] = Enum.sort(gets)
328+
end
329+
330+
test "pops values from a keyword list" do
331+
assert {[1, 2, -3, -4], [c: 4, d: 5]} =
332+
get_and_update_in(@test_list, [Access.values()], fn n ->
333+
if(n > 2, do: {-n, n + 1}, else: :pop)
334+
end)
335+
end
336+
337+
test "raises when not given a map or a keyword list" do
338+
assert_raise RuntimeError, @error_msg_pattern, fn ->
339+
get_in(123, [Access.values()])
340+
end
341+
342+
assert_raise RuntimeError, @error_msg_pattern, fn ->
343+
get_and_update_in(:some_atom, [Access.values()], fn x -> {x, x} end)
344+
end
345+
346+
assert_raise RuntimeError, @error_msg_pattern, fn ->
347+
get_in([:a, :b, :c], [Access.values()])
348+
end
349+
350+
assert_raise RuntimeError, @error_msg_pattern, fn ->
351+
get_in([{:a, :b, :c}, {:d, :e, :f}], [Access.values()])
352+
end
353+
354+
assert_raise RuntimeError, @error_msg_pattern, fn ->
355+
get_in([{1, 2}, {3, 4}], [Access.values()])
356+
end
357+
end
358+
end
295359
end

0 commit comments

Comments
 (0)