Skip to content

Add Access.values/0 #14350

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

Merged
merged 5 commits into from
Mar 24, 2025
Merged
Show file tree
Hide file tree
Changes from 4 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} -> 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} ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should still have guards to be sure it's a keyword?

Suggested change
Enum.map(data, fn {_key, value} -> 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} ->
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) ->

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at Keyword.filter/2 and Keyword.values/1 and they don't seem to be that strict. But why not. 🙂 Added in 8b4aa0f.

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