Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Access.values/0 #14350

Merged
merged 5 commits into from
Mar 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions lib/elixir/lib/access.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
64 changes: 64 additions & 0 deletions lib/elixir/test/elixir/access_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading