Some utilities that I often wish I had are `Access.filter/1` and `Access.find/1`. Often I find myself doing something like
``` list_of_results = get_in(some_list, [:path_a, :path_b]) filtered_results = Enum.filter(list_of_results, &some_predicate/1) get_in(filtered_results, [Access.all(), :a, :b]) ``` The PR w/ documentation and information can be found here: https://github.com/elixir-lang/elixir/pull/6634 The implementation is below. ``` @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: "mary", salary: 20}, %{name: "francine", salary: 30}] iex> get_in(list, [Access.filter(&(&1.salary >= 20)), :name]) ["mary", "francine"] iex> get_and_update_in(list, [Access.filter(&(&1.salary <= 20)), :name], fn ...> prev -> {prev, String.upcase(prev)} ...> end) {["john", "mary"], [%{name: "JOHN", salary: 10}, %{name: "MARY", salary: 20}, %{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: "mary", salary: 20}, %{name: "francine", salary: 30}] iex> pop_in(list, [Access.filter(&(&1.salary >= 20))]) {[%{name: "mary", salary: 20}, %{name: "francine", salary: 30}], [%{name: "john", salary: 10}]} iex> pop_in(list, [Access.filter(&(&1.salary >= 20)), :name]) {["mary", "francine"], [%{name: "john", salary: 10}, %{salary: 20}, %{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: "mary", salary: 20}, %{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: "mary", salary: 20}, %{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(fn a, b -> a == b end)]) ** (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: %{} """ def filter(func) when is_function(func, 1) 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_where(data, func, next, [], []) end defp filter(_op, data, _func, _next) do raise "Access.filter/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: "mary", salary: 20}, %{name: "francine", salary: 30}] iex> get_in(list, [Access.find(&(&1.salary >= 20)), :name]) "mary" iex> get_and_update_in(list, [Access.find(&(&1.salary <= 20)), :name], fn ...> prev -> {prev, String.upcase(prev)} ...> end) {"john", [%{name: "JOHN", salary: 10}, %{name: "mary", salary: 20}, %{name: "francine", salary: 30}]} `find/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: "mary", salary: 20}, %{name: "francine", salary: 30}] iex> pop_in(list, [Access.find(&(&1.salary >= 20))]) {%{name: "mary", salary: 20}, [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]} iex> pop_in(list, [Access.find(&(&1.salary >= 20)), :name]) {"mary", [%{name: "john", salary: 10}, %{salary: 20}, %{name: "francine", 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: "mary", salary: 20}, %{name: "francine", salary: 30}] iex> get_in(list, [Access.find(&(&1.salary >= 50)), :name]) nil iex> get_and_update_in(list, [Access.find(&(&1.salary >= 50)), :name], fn ...> prev -> {prev, String.upcase(prev)} ...> end) {nil, [%{name: "john", salary: 10}, %{name: "mary", salary: 20}, %{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.find(fn a, b -> a == b end)]) ** (FunctionClauseError) no function clause matching in Access.find/1 An error is raised if the accessed structure is not a list: iex> get_in(%{}, [Access.find(fn a -> a == 10 end)]) ** (RuntimeError) Access.find/1 expected a list, got: %{} """ def find(func) when is_function(func, 1) do fn(op, data, next) -> find(op, data, func, next) end end defp find(:get, data, func, next) when is_list(data) do data |> Enum.find(func) |> next.() end defp find(:get_and_update, data, func, next) when is_list(data) do get_and_update_found(data, func, next, []) end defp find(_op, data, _func, _next) do raise "Access.find/1 expected a list, got: #{inspect data}" end defp get_and_update_where([head | rest], func, next, updates, gets) do if func.(head) do case next.(head) do {get, update} -> get_and_update_where(rest, func, next, [update | updates], [get | gets]) :pop -> get_and_update_where(rest, func, next, updates, [head | gets]) end else get_and_update_where(rest, func, next, [head | updates], gets) end end defp get_and_update_where([], _func, _next, updates, gets) do {:list.reverse(gets), :lists.reverse(updates)} end defp get_and_update_found([head | rest], func, next, updates) do if func.(head) do case next.(head) do {get, update} -> {get, :lists.reverse([update | updates], rest)} :pop -> {head, :lists.reverse(updates, rest)} end else get_and_update_found(rest, func, next, [head | updates]) end end defp get_and_update_found([], _index, _next, updates) do {nil, :lists.reverse(updates)} end ``` -- You received this message because you are subscribed to the Google Groups "elixir-lang-core" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/1f77f02e-65f4-440c-a405-417205be2825%40googlegroups.com. For more options, visit https://groups.google.com/d/optout.
