947 lines
30 KiB
Elixir
947 lines
30 KiB
Elixir
defmodule Access do
|
|
@moduledoc """
|
|
Key-based access to data structures.
|
|
|
|
The `Access` module defines a behaviour for dynamically accessing
|
|
keys of any type in a data structure via the `data[key]` syntax.
|
|
|
|
`Access` supports keyword lists (`Keyword`) and maps (`Map`) out
|
|
of the box. Keywords supports only atoms keys, keys for maps can
|
|
be of any type. Both return `nil` if the key does not exist:
|
|
|
|
iex> keywords = [a: 1, b: 2]
|
|
iex> keywords[:a]
|
|
1
|
|
iex> keywords[:c]
|
|
nil
|
|
|
|
iex> map = %{a: 1, b: 2}
|
|
iex> map[:a]
|
|
1
|
|
|
|
iex> star_ratings = %{1.0 => "★", 1.5 => "★☆", 2.0 => "★★"}
|
|
iex> star_ratings[1.5]
|
|
"★☆"
|
|
|
|
This syntax is very convenient as it can be nested arbitrarily:
|
|
|
|
iex> keywords = [a: 1, b: 2]
|
|
iex> keywords[:c][:unknown]
|
|
nil
|
|
|
|
This works because accessing anything on a `nil` value, returns
|
|
`nil` itself:
|
|
|
|
iex> nil[:a]
|
|
nil
|
|
|
|
The access syntax can also be used with the `Kernel.put_in/2`,
|
|
`Kernel.update_in/2` and `Kernel.get_and_update_in/2` macros
|
|
to allow values to be set in nested data structures:
|
|
|
|
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
|
|
iex> put_in(users["john"][:age], 28)
|
|
%{"john" => %{age: 28}, "meg" => %{age: 23}}
|
|
|
|
> Attention! While the access syntax is allowed in maps via
|
|
> `map[key]`, if your map is made of predefined atom keys,
|
|
> you should prefer to access those atom keys with `map.key`
|
|
> instead of `map[key]`, as `map.key` will raise if the key
|
|
> is missing (which is not supposed to happen if the keys are
|
|
> predefined). Similarly, since structs are maps and structs
|
|
> have predefined keys, they only allow the `struct.key`
|
|
> syntax and they do not allow the `struct[key]` access syntax.
|
|
> See the `Map` module for more information.
|
|
|
|
## Nested data structures
|
|
|
|
Both key-based access syntaxes can be used with the nested update
|
|
functions and macros in `Kernel`, such as `Kernel.get_in/2`,
|
|
`Kernel.put_in/3`, `Kernel.update_in/3`, `Kernel.pop_in/2`, and
|
|
`Kernel.get_and_update_in/3`.
|
|
|
|
For example, to update a map inside another map:
|
|
|
|
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
|
|
iex> put_in(users["john"].age, 28)
|
|
%{"john" => %{age: 28}, "meg" => %{age: 23}}
|
|
|
|
This module provides convenience functions for traversing other
|
|
structures, like tuples and lists. These functions can be used
|
|
in all the `Access`-related functions and macros in `Kernel`.
|
|
|
|
For instance, given a user map with the `:name` and `:languages`
|
|
keys, here is how to deeply traverse the map and convert all
|
|
language names to uppercase:
|
|
|
|
iex> languages = [
|
|
...> %{name: "elixir", type: :functional},
|
|
...> %{name: "c", type: :procedural}
|
|
...> ]
|
|
iex> user = %{name: "john", languages: languages}
|
|
iex> update_in(user, [:languages, Access.all(), :name], &String.upcase/1)
|
|
%{
|
|
name: "john",
|
|
languages: [
|
|
%{name: "ELIXIR", type: :functional},
|
|
%{name: "C", type: :procedural}
|
|
]
|
|
}
|
|
|
|
See the functions `key/1`, `key!/1`, `elem/1`, and `all/0` for
|
|
some of the available accessors.
|
|
"""
|
|
|
|
@type container :: keyword | struct | map
|
|
@type nil_container :: nil
|
|
@type t :: container | nil_container | any
|
|
@type key :: any
|
|
@type value :: any
|
|
|
|
@type get_fun(data) ::
|
|
(:get, data, (term -> term) -> new_data :: container)
|
|
|
|
@type get_and_update_fun(data, current_value) ::
|
|
(:get_and_update, data, (term -> term) ->
|
|
{current_value, new_data :: container} | :pop)
|
|
|
|
@type access_fun(data, current_value) ::
|
|
get_fun(data) | get_and_update_fun(data, current_value)
|
|
|
|
@doc """
|
|
Invoked in order to access the value stored under `key` in the given term `term`.
|
|
|
|
This function should return `{:ok, value}` where `value` is the value under
|
|
`key` if the key exists in the term, or `:error` if the key does not exist in
|
|
the term.
|
|
|
|
Many of the functions defined in the `Access` module internally call this
|
|
function. This function is also used when the square-brackets access syntax
|
|
(`structure[key]`) is used: the `fetch/2` callback implemented by the module
|
|
that defines the `structure` struct is invoked and if it returns `{:ok,
|
|
value}` then `value` is returned, or if it returns `:error` then `nil` is
|
|
returned.
|
|
|
|
See the `Map.fetch/2` and `Keyword.fetch/2` implementations for examples of
|
|
how to implement this callback.
|
|
"""
|
|
@callback fetch(term :: t, key) :: {:ok, value} | :error
|
|
|
|
@doc """
|
|
Invoked in order to access the value under `key` and update it at the same time.
|
|
|
|
The implementation of this callback should invoke `fun` with the value under
|
|
`key` in the passed structure `data`, or with `nil` if `key` is not present in it.
|
|
This function must return either `{current_value, new_value}` or `:pop`.
|
|
|
|
If the passed function returns `{current_value, new_value}`,
|
|
the return value of this callback should be `{current_value, new_data}`, where:
|
|
|
|
* `current_value` is the retrieved value (which can be operated on before being returned)
|
|
|
|
* `new_value` is the new value to be stored under `key`
|
|
|
|
* `new_data` is `data` after updating the value of `key` with `new_value`.
|
|
|
|
If the passed function returns `:pop`, the return value of this callback
|
|
must be `{value, new_data}` where `value` is the value under `key`
|
|
(or `nil` if not present) and `new_data` is `data` without `key`.
|
|
|
|
See the implementations of `Map.get_and_update/3` or `Keyword.get_and_update/3`
|
|
for more examples.
|
|
"""
|
|
@callback get_and_update(data, key, (value | nil -> {current_value, new_value :: value} | :pop)) ::
|
|
{current_value, new_data :: data}
|
|
when current_value: value, data: container
|
|
|
|
@doc """
|
|
Invoked to "pop" the value under `key` out of the given data structure.
|
|
|
|
When `key` exists in the given structure `data`, the implementation should
|
|
return a `{value, new_data}` tuple where `value` is the value that was under
|
|
`key` and `new_data` is `term` without `key`.
|
|
|
|
When `key` is not present in the given structure, a tuple `{value, data}`
|
|
should be returned, where `value` is implementation-defined.
|
|
|
|
See the implementations for `Map.pop/3` or `Keyword.pop/3` for more examples.
|
|
"""
|
|
@callback pop(data, key) :: {value, data} when data: container
|
|
|
|
defmacrop raise_undefined_behaviour(exception, module, top) do
|
|
quote do
|
|
exception =
|
|
case __STACKTRACE__ do
|
|
[unquote(top) | _] ->
|
|
reason =
|
|
"#{inspect(unquote(module))} does not implement the Access behaviour. " <>
|
|
"If you are using get_in/put_in/update_in, you can specify the field " <>
|
|
"to be accessed using Access.key!/1"
|
|
|
|
%{unquote(exception) | reason: reason}
|
|
|
|
_ ->
|
|
unquote(exception)
|
|
end
|
|
|
|
reraise exception, __STACKTRACE__
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Fetches the value for the given key in a container (a map, keyword
|
|
list, or struct that implements the `Access` behaviour).
|
|
|
|
Returns `{:ok, value}` where `value` is the value under `key` if there is such
|
|
a key, or `:error` if `key` is not found.
|
|
|
|
## Examples
|
|
|
|
iex> Access.fetch(%{name: "meg", age: 26}, :name)
|
|
{:ok, "meg"}
|
|
|
|
iex> Access.fetch([ordered: true, on_timeout: :exit], :timeout)
|
|
:error
|
|
|
|
"""
|
|
@spec fetch(container, term) :: {:ok, term} | :error
|
|
@spec fetch(nil_container, any) :: :error
|
|
def fetch(container, key)
|
|
|
|
def fetch(%module{} = container, key) do
|
|
module.fetch(container, key)
|
|
rescue
|
|
exception in UndefinedFunctionError ->
|
|
raise_undefined_behaviour(exception, module, {^module, :fetch, [^container, ^key], _})
|
|
end
|
|
|
|
def fetch(map, key) when is_map(map) do
|
|
case map do
|
|
%{^key => value} -> {:ok, value}
|
|
_ -> :error
|
|
end
|
|
end
|
|
|
|
def fetch(list, key) when is_list(list) and is_atom(key) do
|
|
case :lists.keyfind(key, 1, list) do
|
|
{_, value} -> {:ok, value}
|
|
false -> :error
|
|
end
|
|
end
|
|
|
|
def fetch(list, key) when is_list(list) do
|
|
raise ArgumentError,
|
|
"the Access calls for keywords expect the key to be an atom, got: " <> inspect(key)
|
|
end
|
|
|
|
def fetch(nil, _key) do
|
|
:error
|
|
end
|
|
|
|
@doc """
|
|
Same as `fetch/2` but returns the value directly,
|
|
or raises a `KeyError` exception if `key` is not found.
|
|
|
|
## Examples
|
|
|
|
iex> Access.fetch!(%{name: "meg", age: 26}, :name)
|
|
"meg"
|
|
|
|
"""
|
|
@doc since: "1.10.0"
|
|
@spec fetch!(container, term) :: term
|
|
def fetch!(container, key) do
|
|
case fetch(container, key) do
|
|
{:ok, value} -> value
|
|
:error -> raise(KeyError, key: key, term: container)
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Gets the value for the given key in a container (a map, keyword
|
|
list, or struct that implements the `Access` behaviour).
|
|
|
|
Returns the value under `key` if there is such a key, or `default` if `key` is
|
|
not found.
|
|
|
|
## Examples
|
|
|
|
iex> Access.get(%{name: "john"}, :name, "default name")
|
|
"john"
|
|
iex> Access.get(%{name: "john"}, :age, 25)
|
|
25
|
|
|
|
iex> Access.get([ordered: true], :timeout)
|
|
nil
|
|
|
|
"""
|
|
@spec get(container, term, term) :: term
|
|
@spec get(nil_container, any, default) :: default when default: var
|
|
def get(container, key, default \\ nil)
|
|
|
|
# Reimplementing the same logic as Access.fetch/2 here is done for performance, since
|
|
# this is called a lot and calling fetch/2 means introducing some overhead (like
|
|
# building the "{:ok, _}" tuple and deconstructing it back right away).
|
|
|
|
def get(%module{} = container, key, default) do
|
|
try do
|
|
module.fetch(container, key)
|
|
rescue
|
|
exception in UndefinedFunctionError ->
|
|
raise_undefined_behaviour(exception, module, {^module, :fetch, [^container, ^key], _})
|
|
else
|
|
{:ok, value} -> value
|
|
:error -> default
|
|
end
|
|
end
|
|
|
|
def get(map, key, default) when is_map(map) do
|
|
case map do
|
|
%{^key => value} -> value
|
|
_ -> default
|
|
end
|
|
end
|
|
|
|
def get(list, key, default) when is_list(list) and is_atom(key) do
|
|
case :lists.keyfind(key, 1, list) do
|
|
{_, value} -> value
|
|
false -> default
|
|
end
|
|
end
|
|
|
|
def get(list, key, _default) when is_list(list) do
|
|
raise ArgumentError,
|
|
"the Access calls for keywords expect the key to be an atom, got: " <> inspect(key)
|
|
end
|
|
|
|
def get(nil, _key, default) do
|
|
default
|
|
end
|
|
|
|
@doc """
|
|
Gets and updates the given key in a `container` (a map, a keyword list,
|
|
a struct that implements the `Access` behaviour).
|
|
|
|
The `fun` argument receives the value of `key` (or `nil` if `key` is not
|
|
present in `container`) and must return a two-element tuple `{current_value, new_value}`:
|
|
the "get" value `current_value` (the retrieved value, which can be operated on before
|
|
being returned) and the new value to be stored under `key` (`new_value`).
|
|
`fun` may also return `:pop`, which means the current value
|
|
should be removed from the container and returned.
|
|
|
|
The returned value is a two-element tuple with the "get" value returned by
|
|
`fun` and a new container with the updated value under `key`.
|
|
|
|
## Examples
|
|
|
|
iex> Access.get_and_update([a: 1], :a, fn current_value ->
|
|
...> {current_value, current_value + 1}
|
|
...> end)
|
|
{1, [a: 2]}
|
|
|
|
"""
|
|
@spec get_and_update(data, key, (value | nil -> {current_value, new_value :: value} | :pop)) ::
|
|
{current_value, new_data :: data}
|
|
when current_value: var, data: container
|
|
def get_and_update(container, key, fun)
|
|
|
|
def get_and_update(%module{} = container, key, fun) do
|
|
module.get_and_update(container, key, fun)
|
|
rescue
|
|
exception in UndefinedFunctionError ->
|
|
raise_undefined_behaviour(
|
|
exception,
|
|
module,
|
|
{^module, :get_and_update, [^container, ^key, ^fun], _}
|
|
)
|
|
end
|
|
|
|
def get_and_update(map, key, fun) when is_map(map) do
|
|
Map.get_and_update(map, key, fun)
|
|
end
|
|
|
|
def get_and_update(list, key, fun) when is_list(list) do
|
|
Keyword.get_and_update(list, key, fun)
|
|
end
|
|
|
|
def get_and_update(nil, key, _fun) do
|
|
raise ArgumentError, "could not put/update key #{inspect(key)} on a nil value"
|
|
end
|
|
|
|
@doc """
|
|
Removes the entry with a given key from a container (a map, keyword
|
|
list, or struct that implements the `Access` behaviour).
|
|
|
|
Returns a tuple containing the value associated with the key and the
|
|
updated container. `nil` is returned for the value if the key isn't
|
|
in the container.
|
|
|
|
## Examples
|
|
|
|
With a map:
|
|
|
|
iex> Access.pop(%{name: "Elixir", creator: "Valim"}, :name)
|
|
{"Elixir", %{creator: "Valim"}}
|
|
|
|
A keyword list:
|
|
|
|
iex> Access.pop([name: "Elixir", creator: "Valim"], :name)
|
|
{"Elixir", [creator: "Valim"]}
|
|
|
|
An unknown key:
|
|
|
|
iex> Access.pop(%{name: "Elixir", creator: "Valim"}, :year)
|
|
{nil, %{creator: "Valim", name: "Elixir"}}
|
|
|
|
"""
|
|
@spec pop(data, key) :: {value, data} when data: container
|
|
def pop(%module{} = container, key) do
|
|
module.pop(container, key)
|
|
rescue
|
|
exception in UndefinedFunctionError ->
|
|
raise_undefined_behaviour(exception, module, {^module, :pop, [^container, ^key], _})
|
|
end
|
|
|
|
def pop(map, key) when is_map(map) do
|
|
Map.pop(map, key)
|
|
end
|
|
|
|
def pop(list, key) when is_list(list) do
|
|
Keyword.pop(list, key)
|
|
end
|
|
|
|
def pop(nil, key) do
|
|
raise ArgumentError, "could not pop key #{inspect(key)} on a nil value"
|
|
end
|
|
|
|
## Accessors
|
|
|
|
@doc """
|
|
Returns a function that accesses the given key in a map/struct.
|
|
|
|
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
|
`Kernel.get_and_update_in/3`, and friends.
|
|
|
|
The returned function uses the default value if the key does not exist.
|
|
This can be used to specify defaults and safely traverse missing keys:
|
|
|
|
iex> get_in(%{}, [Access.key(:user, %{}), Access.key(:name, "meg")])
|
|
"meg"
|
|
|
|
Such is also useful when using update functions, allowing us to introduce
|
|
values as we traverse the data structure for updates:
|
|
|
|
iex> put_in(%{}, [Access.key(:user, %{}), Access.key(:name)], "Mary")
|
|
%{user: %{name: "Mary"}}
|
|
|
|
## Examples
|
|
|
|
iex> map = %{user: %{name: "john"}}
|
|
iex> get_in(map, [Access.key(:unknown, %{}), Access.key(:name, "john")])
|
|
"john"
|
|
iex> get_and_update_in(map, [Access.key(:user), Access.key(:name)], fn prev ->
|
|
...> {prev, String.upcase(prev)}
|
|
...> end)
|
|
{"john", %{user: %{name: "JOHN"}}}
|
|
iex> pop_in(map, [Access.key(:user), Access.key(:name)])
|
|
{"john", %{user: %{}}}
|
|
|
|
An error is raised if the accessed structure is not a map or a struct:
|
|
|
|
iex> get_in([], [Access.key(:foo)])
|
|
** (BadMapError) expected a map, got: []
|
|
|
|
"""
|
|
@spec key(key, term) :: access_fun(data :: struct | map, current_value :: term)
|
|
def key(key, default \\ nil) do
|
|
fn
|
|
:get, data, next ->
|
|
next.(Map.get(data, key, default))
|
|
|
|
:get_and_update, data, next ->
|
|
value = Map.get(data, key, default)
|
|
|
|
case next.(value) do
|
|
{get, update} -> {get, Map.put(data, key, update)}
|
|
:pop -> {value, Map.delete(data, key)}
|
|
end
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Returns a function that accesses the given key in a map/struct.
|
|
|
|
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
|
`Kernel.get_and_update_in/3`, and friends.
|
|
|
|
Similar to `key/2`, but the returned function raises if the key does not exist.
|
|
|
|
## Examples
|
|
|
|
iex> map = %{user: %{name: "john"}}
|
|
iex> get_in(map, [Access.key!(:user), Access.key!(:name)])
|
|
"john"
|
|
iex> get_and_update_in(map, [Access.key!(:user), Access.key!(:name)], fn prev ->
|
|
...> {prev, String.upcase(prev)}
|
|
...> end)
|
|
{"john", %{user: %{name: "JOHN"}}}
|
|
iex> pop_in(map, [Access.key!(:user), Access.key!(:name)])
|
|
{"john", %{user: %{}}}
|
|
iex> get_in(map, [Access.key!(:user), Access.key!(:unknown)])
|
|
** (KeyError) key :unknown not found in: %{name: \"john\"}
|
|
|
|
An error is raised if the accessed structure is not a map/struct:
|
|
|
|
iex> get_in([], [Access.key!(:foo)])
|
|
** (RuntimeError) Access.key!/1 expected a map/struct, got: []
|
|
|
|
"""
|
|
@spec key!(key) :: access_fun(data :: struct | map, current_value :: term)
|
|
def key!(key) do
|
|
fn
|
|
:get, %{} = data, next ->
|
|
next.(Map.fetch!(data, key))
|
|
|
|
:get_and_update, %{} = data, next ->
|
|
value = Map.fetch!(data, key)
|
|
|
|
case next.(value) do
|
|
{get, update} -> {get, Map.put(data, key, update)}
|
|
:pop -> {value, Map.delete(data, key)}
|
|
end
|
|
|
|
_op, data, _next ->
|
|
raise "Access.key!/1 expected a map/struct, got: #{inspect(data)}"
|
|
end
|
|
end
|
|
|
|
@doc ~S"""
|
|
Returns a function that accesses the element at the given index in a tuple.
|
|
|
|
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
|
`Kernel.get_and_update_in/3`, and friends.
|
|
|
|
The returned function raises if `index` is out of bounds.
|
|
|
|
Note that popping elements out of tuples is not possible and raises an
|
|
error.
|
|
|
|
## Examples
|
|
|
|
iex> map = %{user: {"john", 27}}
|
|
iex> get_in(map, [:user, Access.elem(0)])
|
|
"john"
|
|
iex> get_and_update_in(map, [:user, Access.elem(0)], fn prev ->
|
|
...> {prev, String.upcase(prev)}
|
|
...> end)
|
|
{"john", %{user: {"JOHN", 27}}}
|
|
iex> pop_in(map, [:user, Access.elem(0)])
|
|
** (RuntimeError) cannot pop data from a tuple
|
|
|
|
An error is raised if the accessed structure is not a tuple:
|
|
|
|
iex> get_in(%{}, [Access.elem(0)])
|
|
** (RuntimeError) Access.elem/1 expected a tuple, got: %{}
|
|
|
|
"""
|
|
@spec elem(non_neg_integer) :: access_fun(data :: tuple, current_value :: term)
|
|
def elem(index) when is_integer(index) and index >= 0 do
|
|
pos = index + 1
|
|
|
|
fn
|
|
:get, data, next when is_tuple(data) ->
|
|
next.(:erlang.element(pos, data))
|
|
|
|
:get_and_update, data, next when is_tuple(data) ->
|
|
value = :erlang.element(pos, data)
|
|
|
|
case next.(value) do
|
|
{get, update} -> {get, :erlang.setelement(pos, data, update)}
|
|
:pop -> raise "cannot pop data from a tuple"
|
|
end
|
|
|
|
_op, data, _next ->
|
|
raise "Access.elem/1 expected a tuple, got: #{inspect(data)}"
|
|
end
|
|
end
|
|
|
|
@doc ~S"""
|
|
Returns a function that accesses all the elements in a 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> list = [%{name: "john"}, %{name: "mary"}]
|
|
iex> get_in(list, [Access.all(), :name])
|
|
["john", "mary"]
|
|
iex> get_and_update_in(list, [Access.all(), :name], fn prev ->
|
|
...> {prev, String.upcase(prev)}
|
|
...> end)
|
|
{["john", "mary"], [%{name: "JOHN"}, %{name: "MARY"}]}
|
|
iex> pop_in(list, [Access.all(), :name])
|
|
{["john", "mary"], [%{}, %{}]}
|
|
|
|
Here is an example that traverses the list dropping even
|
|
numbers and multiplying odd numbers by 2:
|
|
|
|
iex> require Integer
|
|
iex> get_and_update_in([1, 2, 3, 4, 5], [Access.all()], fn num ->
|
|
...> if Integer.is_even(num), do: :pop, else: {num, num * 2}
|
|
...> end)
|
|
{[1, 2, 3, 4, 5], [2, 6, 10]}
|
|
|
|
An error is raised if the accessed structure is not a list:
|
|
|
|
iex> get_in(%{}, [Access.all()])
|
|
** (RuntimeError) Access.all/0 expected a list, got: %{}
|
|
|
|
"""
|
|
@spec all() :: access_fun(data :: list, current_value :: list)
|
|
def all() do
|
|
&all/3
|
|
end
|
|
|
|
defp all(:get, data, next) when is_list(data) do
|
|
Enum.map(data, next)
|
|
end
|
|
|
|
defp all(:get_and_update, data, next) when is_list(data) do
|
|
all(data, next, _gets = [], _updates = [])
|
|
end
|
|
|
|
defp all(_op, data, _next) do
|
|
raise "Access.all/0 expected a list, got: #{inspect(data)}"
|
|
end
|
|
|
|
defp all([head | rest], next, gets, updates) do
|
|
case next.(head) do
|
|
{get, update} -> all(rest, next, [get | gets], [update | updates])
|
|
:pop -> all(rest, next, [head | gets], updates)
|
|
end
|
|
end
|
|
|
|
defp all([], _next, gets, updates) do
|
|
{:lists.reverse(gets), :lists.reverse(updates)}
|
|
end
|
|
|
|
@doc ~S"""
|
|
Returns a function that accesses the element at `index` (zero based) of a 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> list = [%{name: "john"}, %{name: "mary"}]
|
|
iex> get_in(list, [Access.at(1), :name])
|
|
"mary"
|
|
iex> get_in(list, [Access.at(-1), :name])
|
|
"mary"
|
|
iex> get_and_update_in(list, [Access.at(0), :name], fn prev ->
|
|
...> {prev, String.upcase(prev)}
|
|
...> end)
|
|
{"john", [%{name: "JOHN"}, %{name: "mary"}]}
|
|
iex> get_and_update_in(list, [Access.at(-1), :name], fn prev ->
|
|
...> {prev, String.upcase(prev)}
|
|
...> end)
|
|
{"mary", [%{name: "john"}, %{name: "MARY"}]}
|
|
|
|
`at/1` can also be used to pop elements out of a list or
|
|
a key inside of a list:
|
|
|
|
iex> list = [%{name: "john"}, %{name: "mary"}]
|
|
iex> pop_in(list, [Access.at(0)])
|
|
{%{name: "john"}, [%{name: "mary"}]}
|
|
iex> pop_in(list, [Access.at(0), :name])
|
|
{"john", [%{}, %{name: "mary"}]}
|
|
|
|
When the index is out of bounds, `nil` is returned and the update function is never called:
|
|
|
|
iex> list = [%{name: "john"}, %{name: "mary"}]
|
|
iex> get_in(list, [Access.at(10), :name])
|
|
nil
|
|
iex> get_and_update_in(list, [Access.at(10), :name], fn prev ->
|
|
...> {prev, String.upcase(prev)}
|
|
...> end)
|
|
{nil, [%{name: "john"}, %{name: "mary"}]}
|
|
|
|
An error is raised if the accessed structure is not a list:
|
|
|
|
iex> get_in(%{}, [Access.at(1)])
|
|
** (RuntimeError) Access.at/1 expected a list, got: %{}
|
|
|
|
"""
|
|
@spec at(integer) :: access_fun(data :: list, current_value :: term)
|
|
def at(index) when is_integer(index) do
|
|
fn op, data, next -> at(op, data, index, next) end
|
|
end
|
|
|
|
defp at(:get, data, index, next) when is_list(data) do
|
|
data |> Enum.at(index) |> next.()
|
|
end
|
|
|
|
defp at(:get_and_update, data, index, next) when is_list(data) do
|
|
get_and_update_at(data, index, next, [], fn -> nil end)
|
|
end
|
|
|
|
defp at(_op, data, _index, _next) do
|
|
raise "Access.at/1 expected a list, got: #{inspect(data)}"
|
|
end
|
|
|
|
defp get_and_update_at([head | rest], 0, next, updates, _default_fun) do
|
|
case next.(head) do
|
|
{get, update} -> {get, :lists.reverse([update | updates], rest)}
|
|
:pop -> {head, :lists.reverse(updates, rest)}
|
|
end
|
|
end
|
|
|
|
defp get_and_update_at([_ | _] = list, index, next, updates, default_fun) when index < 0 do
|
|
list_length = length(list)
|
|
|
|
if list_length + index >= 0 do
|
|
get_and_update_at(list, list_length + index, next, updates, default_fun)
|
|
else
|
|
{default_fun.(), list}
|
|
end
|
|
end
|
|
|
|
defp get_and_update_at([head | rest], index, next, updates, default_fun) when index > 0 do
|
|
get_and_update_at(rest, index - 1, next, [head | updates], default_fun)
|
|
end
|
|
|
|
defp get_and_update_at([], _index, _next, updates, default_fun) do
|
|
{default_fun.(), :lists.reverse(updates)}
|
|
end
|
|
|
|
@doc ~S"""
|
|
Same as `at/1` except that it raises `Enum.OutOfBoundsError`
|
|
if the given index is out of bounds.
|
|
|
|
## Examples
|
|
|
|
iex> get_in([:a, :b, :c], [Access.at!(2)])
|
|
:c
|
|
iex> get_in([:a, :b, :c], [Access.at!(3)])
|
|
** (Enum.OutOfBoundsError) out of bounds error
|
|
|
|
"""
|
|
@doc since: "1.11.0"
|
|
@spec at!(integer) :: access_fun(data :: list, current_value :: term)
|
|
def at!(index) when is_integer(index) do
|
|
fn op, data, next -> at!(op, data, index, next) end
|
|
end
|
|
|
|
defp at!(:get, data, index, next) when is_list(data) do
|
|
case Enum.fetch(data, index) do
|
|
{:ok, value} -> next.(value)
|
|
:error -> raise Enum.OutOfBoundsError
|
|
end
|
|
end
|
|
|
|
defp at!(:get_and_update, data, index, next) when is_list(data) do
|
|
get_and_update_at(data, index, next, [], fn -> raise Enum.OutOfBoundsError end)
|
|
end
|
|
|
|
defp at!(_op, data, _index, _next) do
|
|
raise "Access.at!/1 expected a list, got: #{inspect(data)}"
|
|
end
|
|
|
|
@doc ~S"""
|
|
Returns a function that accesses all elements of a list that match the provided predicate.
|
|
|
|
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
|
`Kernel.get_and_update_in/3`, and friends.
|
|
|
|
## Examples
|
|
|
|
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
|
|
iex> get_in(list, [Access.filter(&(&1.salary > 20)), :name])
|
|
["francine"]
|
|
iex> get_and_update_in(list, [Access.filter(&(&1.salary <= 20)), :name], fn prev ->
|
|
...> {prev, String.upcase(prev)}
|
|
...> end)
|
|
{["john"], [%{name: "JOHN", salary: 10}, %{name: "francine", salary: 30}]}
|
|
|
|
`filter/1` can also be used to pop elements out of a list or
|
|
a key inside of a list:
|
|
|
|
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
|
|
iex> pop_in(list, [Access.filter(&(&1.salary >= 20))])
|
|
{[%{name: "francine", salary: 30}], [%{name: "john", salary: 10}]}
|
|
iex> pop_in(list, [Access.filter(&(&1.salary >= 20)), :name])
|
|
{["francine"], [%{name: "john", salary: 10}, %{salary: 30}]}
|
|
|
|
When no match is found, an empty list is returned and the update function is never called
|
|
|
|
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
|
|
iex> get_in(list, [Access.filter(&(&1.salary >= 50)), :name])
|
|
[]
|
|
iex> get_and_update_in(list, [Access.filter(&(&1.salary >= 50)), :name], fn prev ->
|
|
...> {prev, String.upcase(prev)}
|
|
...> end)
|
|
{[], [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
|
|
|
|
An error is raised if the predicate is not a function or is of the incorrect arity:
|
|
|
|
iex> get_in([], [Access.filter(5)])
|
|
** (FunctionClauseError) no function clause matching in Access.filter/1
|
|
|
|
An error is raised if the accessed structure is not a list:
|
|
|
|
iex> get_in(%{}, [Access.filter(fn a -> a == 10 end)])
|
|
** (RuntimeError) Access.filter/1 expected a list, got: %{}
|
|
|
|
"""
|
|
@doc since: "1.6.0"
|
|
@spec filter((term -> boolean)) :: access_fun(data :: list, current_value :: list)
|
|
def filter(func) when is_function(func) do
|
|
fn op, data, next -> filter(op, data, func, next) end
|
|
end
|
|
|
|
defp filter(:get, data, func, next) when is_list(data) do
|
|
data |> Enum.filter(func) |> Enum.map(next)
|
|
end
|
|
|
|
defp filter(:get_and_update, data, func, next) when is_list(data) do
|
|
get_and_update_filter(data, func, next, [], [])
|
|
end
|
|
|
|
defp filter(_op, data, _func, _next) do
|
|
raise "Access.filter/1 expected a list, got: #{inspect(data)}"
|
|
end
|
|
|
|
defp get_and_update_filter([head | rest], func, next, updates, gets) do
|
|
if func.(head) do
|
|
case next.(head) do
|
|
{get, update} ->
|
|
get_and_update_filter(rest, func, next, [update | updates], [get | gets])
|
|
|
|
:pop ->
|
|
get_and_update_filter(rest, func, next, updates, [head | gets])
|
|
end
|
|
else
|
|
get_and_update_filter(rest, func, next, [head | updates], gets)
|
|
end
|
|
end
|
|
|
|
defp get_and_update_filter([], _func, _next, updates, gets) do
|
|
{:lists.reverse(gets), :lists.reverse(updates)}
|
|
end
|
|
|
|
@doc ~S"""
|
|
Returns a function that accesses all items of a list that are within the provided range.
|
|
|
|
The range will be normalized following the same rules from `Enum.slice/2`.
|
|
|
|
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
|
`Kernel.get_and_update_in/3`, and friends.
|
|
|
|
## Examples
|
|
|
|
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}, %{name: "vitor", salary: 25}]
|
|
iex> get_in(list, [Access.slice(1..2), :name])
|
|
["francine", "vitor"]
|
|
iex> get_and_update_in(list, [Access.slice(1..3//2), :name], fn prev ->
|
|
...> {prev, String.upcase(prev)}
|
|
...> end)
|
|
{["francine"], [%{name: "john", salary: 10}, %{name: "FRANCINE", salary: 30}, %{name: "vitor", salary: 25}]}
|
|
|
|
`slice/1` can also be used to pop elements out of a list or
|
|
a key inside of a list:
|
|
|
|
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}, %{name: "vitor", salary: 25}]
|
|
iex> pop_in(list, [Access.slice(-2..-1)])
|
|
{[%{name: "francine", salary: 30}, %{name: "vitor", salary: 25}], [%{name: "john", salary: 10}]}
|
|
iex> pop_in(list, [Access.slice(-2..-1), :name])
|
|
{["francine", "vitor"], [%{name: "john", salary: 10}, %{salary: 30}, %{salary: 25}]}
|
|
|
|
When no match is found, an empty list is returned and the update function is never called
|
|
|
|
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}, %{name: "vitor", salary: 25}]
|
|
iex> get_in(list, [Access.slice(5..10//2), :name])
|
|
[]
|
|
iex> get_and_update_in(list, [Access.slice(5..10//2), :name], fn prev ->
|
|
...> {prev, String.upcase(prev)}
|
|
...> end)
|
|
{[], [%{name: "john", salary: 10}, %{name: "francine", salary: 30}, %{name: "vitor", salary: 25}]}
|
|
|
|
An error is raised if the accessed structure is not a list:
|
|
|
|
iex> get_in(%{}, [Access.slice(2..10//3)])
|
|
** (ArgumentError) Access.slice/1 expected a list, got: %{}
|
|
|
|
An error is raised if the step of the range is negative:
|
|
|
|
iex> get_in([], [Access.slice(2..10//-1)])
|
|
** (ArgumentError) Access.slice/1 does not accept ranges with negative steps, got: 2..10//-1
|
|
|
|
"""
|
|
@doc since: "1.14"
|
|
@spec slice(Range.t()) :: access_fun(data :: list, current_value :: list)
|
|
def slice(%Range{} = range) do
|
|
if range.step > 0 do
|
|
fn op, data, next -> slice(op, data, range, next) end
|
|
else
|
|
raise ArgumentError,
|
|
"Access.slice/1 does not accept ranges with negative steps, got: #{inspect(range)}"
|
|
end
|
|
end
|
|
|
|
defp slice(:get, data, %Range{} = range, next) when is_list(data) do
|
|
data
|
|
|> Enum.slice(range)
|
|
|> Enum.map(next)
|
|
end
|
|
|
|
defp slice(:get_and_update, data, range, next) when is_list(data) do
|
|
range = normalize_range(range, data)
|
|
|
|
if range.first > range.last do
|
|
{[], data}
|
|
else
|
|
get_and_update_slice(data, range, next, [], [], 0)
|
|
end
|
|
end
|
|
|
|
defp slice(_op, data, _range, _next) do
|
|
raise ArgumentError, "Access.slice/1 expected a list, got: #{inspect(data)}"
|
|
end
|
|
|
|
defp normalize_range(%Range{first: first, last: last, step: step}, list)
|
|
when first < 0 or last < 0 do
|
|
count = length(list)
|
|
first = if first >= 0, do: first, else: Kernel.max(first + count, 0)
|
|
last = if last >= 0, do: last, else: last + count
|
|
Range.new(first, last, step)
|
|
end
|
|
|
|
defp normalize_range(range, _list), do: range
|
|
|
|
defp get_and_update_slice([head | rest], range, next, updates, gets, index) do
|
|
if index in range do
|
|
case next.(head) do
|
|
:pop ->
|
|
get_and_update_slice(rest, range, next, updates, [head | gets], index + 1)
|
|
|
|
{get, update} ->
|
|
get_and_update_slice(
|
|
rest,
|
|
range,
|
|
next,
|
|
[update | updates],
|
|
[get | gets],
|
|
index + 1
|
|
)
|
|
end
|
|
else
|
|
get_and_update_slice(rest, range, next, [head | updates], gets, index + 1)
|
|
end
|
|
end
|
|
|
|
defp get_and_update_slice([], _range, _next, updates, gets, _index) do
|
|
{:lists.reverse(gets), :lists.reverse(updates)}
|
|
end
|
|
end
|