Access.filter not working: (RuntimeError) Access.at/1 expected a list, got: %{"amount" => 10}

Apologies for the formatting. i cant figure out why this wouldnt. Its clear the result of access.filter is a list, but then when chained, it isnt ?!?!

iex(3)> list = %{"children" => [%{"amount"=> 5}, %{"amount" => 10}, %{"amount" => 15}]}
%{"children" => [%{"amount" => 5}, %{"amount" => 10}, %{"amount" => 15}]}
iex(4)> get_in(list, ["children", Access.filter(&(&1["amount"] > 9))])
[%{"amount" => 10}, %{"amount" => 15}]
iex(5)> get_in(list, ["children", Access.filter(&(&1["amount"] > 9)), Access.at(1)])
** (RuntimeError) Access.at/1 expected a list, got: %{"amount" => 10}
    (elixir 1.17.2) lib/access.ex:773: Access.at/4
    (elixir 1.17.2) lib/enum.ex:1703: Enum."-map/2-lists^map/1-1-"/2
    iex:5: (file)
iex(5)>

iirc Access.filter is like Access.all, it does not really return a list, but taps into the list it is given, and sends each item to the next “accessor” so it could access it.

For example, when you did get_in(list, ["children", Access.filter(fn i -> i["amount"] > 9 end)]) then when "children" (Accessor) is encountered, then list["children"] is passed to the next one, which was Access.filter.

However, when Access.filter is encountered, it would be like a foreach where each item the predicate returns true for will be accessed by the next in command.

Now, Access.at expects a List when it is encountered, so, if you did get_in(list, ["children", Access.at(0)] you would get a result, as list["children"] is a list, however, when you hit Access.filter, it is basically a function that is throwing one by one for the next item to “access” and “collect”, and that’s a function, not a list. That’s why Access.at won’t work. However, if you put "amount" it would work, because it would do a [] on each returned element, and get it.

If you wanted 1-th item, you could get out of get_in and do an Enum.at(1) perhaps. Not sure if get_in would have a visually appealing way of doing it, but I could be wrong.

3 Likes

I’m not sure if it’s relevant for what you’re trying to achieve, but mentioning it just in case.
If you want the first matching element (at(0)) instead of the second one (at(1)), the newly added Access.find/1 might be what you need:

iex> get_in(list, ["children", Access.find(&(&1["amount"] > 9))])
%{"amount" => 10}

Otherwise using a regular pipeline of functions might be better than access indeed.

3 Likes

Here’s a lightly-modified version of Access.filter that does what you want:

defmodule FilterWhole do
  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) |> next.()
  end

  defp filter(:get_and_update, data, func, next) when is_list(data) do
    # need to call next.(data) and then handle tuple or :pop... somehow
    raise 'what should happen here'
  end

  defp filter(_op, data, _func, _next) do
    raise "FilterWhole.filter/1 expected a list, got: #{inspect(data)}"
  end
end

data = %{
  children: [%{amount: 1}, %{amount: 2}, %{amount: 3}]
}

result = get_in data, [:children, FilterWhole.filter(fn %{amount: x} -> rem(x, 2) > 0 end), Access.at(1)]

However, I’m not sure what doing a pop or an update means in this situation so that part of the function doesn’t work.