diff --git a/lib/elixir/lib/access.ex b/lib/elixir/lib/access.ex index e5cfdca15e..292e76c1ff 100644 --- a/lib/elixir/lib/access.ex +++ b/lib/elixir/lib/access.ex @@ -999,6 +999,93 @@ defmodule Access do raise ArgumentError, "Access.slice/1 expected a list, got: #{inspect(data)}" end + @doc """ + Returns a function that accesses all values in a map or a keyword list. + + The returned function is typically passed as an accessor to `Kernel.get_in/2`, + `Kernel.get_and_update_in/3`, and friends. + + ## Examples + + iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}} + iex> get_in(users, [Access.values(), :age]) |> Enum.sort() + [23, 27] + iex> update_in(users, [Access.values(), :age], fn age -> age + 1 end) + %{"john" => %{age: 28}, "meg" => %{age: 24}} + iex> put_in(users, [Access.values(), :planet], "Earth") + %{"john" => %{age: 27, planet: "Earth"}, "meg" => %{age: 23, planet: "Earth"}} + + Values in keyword lists can be accessed as well: + + iex> users = [john: %{age: 27}, meg: %{age: 23}] + iex> get_and_update_in(users, [Access.values(), :age], fn age -> {age, age + 1} end) + {[27, 23], [john: %{age: 28}, meg: %{age: 24}]} + + By returning `:pop` from an accessor function, you can remove the accessed key and value + from the map or keyword list: + + iex> require Integer + iex> numbers = [one: 1, two: 2, three: 3, four: 4] + iex> get_and_update_in(numbers, [Access.values()], fn num -> + ...> if Integer.is_even(num), do: :pop, else: {num, to_string(num)} + ...> end) + {[1, 2, 3, 4], [one: "1", three: "3"]} + + An error is raised if the accessed structure is not a map nor a keyword list: + + iex> get_in([1, 2, 3], [Access.values()]) + ** (RuntimeError) Access.values/0 expected a map or a keyword list, got: [1, 2, 3] + """ + @doc since: "1.19.0" + @spec values() :: Access.access_fun(data :: map() | keyword(), current_value :: list()) + def values do + &values/3 + end + + defp values(:get, data = %{}, next) do + Enum.map(data, fn {_key, value} -> next.(value) end) + end + + defp values(:get_and_update, data = %{}, next) do + {reverse_gets, updated_data} = + Enum.reduce(data, {[], %{}}, fn {key, value}, {gets, data_acc} -> + case next.(value) do + {get, update} -> {[get | gets], Map.put(data_acc, key, update)} + :pop -> {[value | gets], data_acc} + end + end) + + {Enum.reverse(reverse_gets), updated_data} + end + + defp values(op, data = [], next) do + values_keyword(op, data, next) + end + + defp values(op, data = [{key, _value} | _tail], next) when is_atom(key) do + values_keyword(op, data, next) + end + + defp values(_op, data, _next) do + raise "Access.values/0 expected a map or a keyword list, got: #{inspect(data)}" + end + + defp values_keyword(:get, data, next) do + Enum.map(data, fn {key, value} when is_atom(key) -> next.(value) end) + end + + defp values_keyword(:get_and_update, data, next) do + {reverse_gets, reverse_updated_data} = + Enum.reduce(data, {[], []}, fn {key, value}, {gets, data_acc} when is_atom(key) -> + case next.(value) do + {get, update} -> {[get | gets], [{key, update} | data_acc]} + :pop -> {[value | gets], data_acc} + end + end) + + {Enum.reverse(reverse_gets), Enum.reverse(reverse_updated_data)} + end + defp normalize_range(%Range{first: first, last: last, step: step}, list) when first < 0 or last < 0 do count = length(list) diff --git a/lib/elixir/test/elixir/access_test.exs b/lib/elixir/test/elixir/access_test.exs index 9ff5a0ce9f..e23102da63 100644 --- a/lib/elixir/test/elixir/access_test.exs +++ b/lib/elixir/test/elixir/access_test.exs @@ -292,4 +292,68 @@ defmodule AccessTest do assert get_in(input, [:list, Access.at!(0), :greeting]) == "hi" end end + + describe "values/0" do + @test_map %{a: 1, b: 2, c: 3, d: 4} + @test_list [a: 1, b: 2, c: 3, d: 4] + @error_msg_pattern ~r[^Access.values/0 expected a map or a keyword list, got: .*] + + test "retrieves values in a map" do + assert [1, 2, 3, 4] = get_in(@test_map, [Access.values()]) |> Enum.sort() + end + + test "retrieves values in a keyword list" do + assert [1, 2, 3, 4] = get_in(@test_list, [Access.values()]) + end + + test "gets and updates values in a map" do + assert {gets, %{a: 3, b: 4, c: 5, d: 6}} = + get_and_update_in(@test_map, [Access.values()], fn n -> {n + 1, n + 2} end) + + assert [2, 3, 4, 5] = Enum.sort(gets) + end + + test "gets and updates values in a keyword list" do + assert {[2, 3, 4, 5], [a: 3, b: 4, c: 5, d: 6]} = + get_and_update_in(@test_list, [Access.values()], fn n -> {n + 1, n + 2} end) + end + + test "pops values from a map" do + assert {gets, %{c: 4, d: 5}} = + get_and_update_in(@test_map, [Access.values()], fn n -> + if(n > 2, do: {-n, n + 1}, else: :pop) + end) + + assert [-4, -3, 1, 2] = Enum.sort(gets) + end + + test "pops values from a keyword list" do + assert {[1, 2, -3, -4], [c: 4, d: 5]} = + get_and_update_in(@test_list, [Access.values()], fn n -> + if(n > 2, do: {-n, n + 1}, else: :pop) + end) + end + + test "raises when not given a map or a keyword list" do + assert_raise RuntimeError, @error_msg_pattern, fn -> + get_in(123, [Access.values()]) + end + + assert_raise RuntimeError, @error_msg_pattern, fn -> + get_and_update_in(:some_atom, [Access.values()], fn x -> {x, x} end) + end + + assert_raise RuntimeError, @error_msg_pattern, fn -> + get_in([:a, :b, :c], [Access.values()]) + end + + assert_raise RuntimeError, @error_msg_pattern, fn -> + get_in([{:a, :b, :c}, {:d, :e, :f}], [Access.values()]) + end + + assert_raise RuntimeError, @error_msg_pattern, fn -> + get_in([{1, 2}, {3, 4}], [Access.values()]) + end + end + end end