[Proposal] Adding Access.find/1

38 views
Skip to first unread message

oliver....@googlemail.com

unread,
Mar 19, 2024, 1:30:30 PMMar 19
to elixir-lang-core
Hi.

I already made a PR but was redirected here. ;-)

This new function Access.find/1 would basically work like Enum.find/2 but for get_in/2 and similar functions.

It can be used for scenarios like:
- Popping the first found element.
- Updating only the first found match in a list.
- To get_in/2 an element directly instead of piping from get_in/2 into Enum.find/2.

The implementation is very similar to Access.filter/1 and Access.at/1.

We added this functions as utility function in our own project because we couldn't really find an elegant way to do such pointed updates with the existing functions.

These are the examples I would have included in the doc string:

      iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
      iex> get_in(list, [Access.find(&(&1.salary > 20)), :name])
      "francine"

      iex>  get_and_update_in(list, [Access.find(&(&1.salary <= 40)), :name], fn prev ->
      ...> {prev, String.upcase(prev)}
      ...>  end)
      {"john", [%{name: "JOHN", salary: 10}, %{name: "francine", salary: 30}]}

      iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
      iex> pop_in(list, [Access.find(&(&1.salary <= 40))])
      {%{name: "john", salary: 10}, [%{name: "francine", salary: 30}]}

      iex> list = [%{name: "john", salary: 10}, %{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: "francine", salary: 30}]}


Thanks,
Oliver

Jean Klingler

unread,
Mar 19, 2024, 7:17:20 PMMar 19
to elixir-l...@googlegroups.com
I like it. It would be to `Access.filter` what `Enum.find` is to `Enum.filter`.
I think it would be a nice addition as it can express operations that would be quite verbose otherwise.

--
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 elixir-lang-co...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-lang-core/44ed5beb-1730-46d7-931a-217825cc4432n%40googlegroups.com.

Andrea Leopardi

unread,
Mar 20, 2024, 5:07:33 AMMar 20
to elixir-l...@googlegroups.com
Right now I’m a bit drowning in work but IIRC there already was a proposal for this, has anyone searched the mailing list?

oliver....@googlemail.com

unread,
Mar 20, 2024, 6:37:19 AMMar 20
to elixir-lang-core
Hello, okay I checked.

Well, there was a discussion 7 years ago when Access.filter/1 was introduced but Access.find/1 was not.

Maybe opinions might have changed since then?

When going into the PR from back then I find the reasoning not very strong on not merging Access.find/1 because "it could be expressed by a more strictly defined Access.filter/1". 

I don't find that to be true. It has pretty much has the same use cases as Enum.find/2 when used with get_in/2, for example. 

Writing a very convoluted filter predicate to catch only the first occurrence when you really need to do that - we basically found that to be very unelegant. I really tried to cram our use case into the Access.filter/1 approach and it was not good.

An added benefit is that we do not walk the rest of the list - once an element is found, the tail is just appended in updates. It has therefore a slightly better performance for its specific use case over Access.filter/1. You also don't get a list you have to Access.all after. I mean, it's basically like Enum.find/2 instead of Enum.filter/2.

Btw, our use case was as follows:

We have a data structure representing a testcase to be run. Later on we want to verify some counter updates done in that TC. We reuse the data structure describing the TC. For this particular requirement regarding the counter updates only the first occurrence of a particular procedure will behave different. It has no other criteria it is different or can be told apart by, so we just update the first occurrence for this check with a flag for easier post-processing of the counter data. This flag has no relevance to other parts of our testing system, and if TC authors add it manually, they might forget. We simply use internally Access.find/1 to specifically pick that one.

When you have very clear, distinct criteria like in a DB row update (like there's only one "Jane Smith" with ID 42) then there would be no be advantage over Access.filter/1. So it's situational, but in the situations it's useful it's hard to express otherwise. For example once you Access.filter/1 you can no longer do something like Access.at/1 because it directly moves you into the elements.

Sorry for the long post, but I hope it conveys the rationale.

Best regards, 
Oliver

José Valim

unread,
Mar 20, 2024, 6:40:15 AMMar 20
to elixir-l...@googlegroups.com
Can you please provide a link to the previous discussions? I recall dealing with some complexities around finding and not finding elements as well. Thanks!

oliver....@googlemail.com

unread,
Mar 20, 2024, 7:00:21 AMMar 20
to elixir-lang-core
This is what I found:

From the original PR: https://github.com/elixir-lang/elixir/pull/6634 (this has a lengthy discussion on the merits).

The original discussion about including both: https://groups.google.com/g/elixir-lang-core/c/LlZCz0iYgfc/m/5XLRvg8XAgAJ (not very detailed, discussion happened in PR it seems).

A discussion from one before that: https://groups.google.com/g/elixir-lang-core/c/WtKXtP0XFqc/m/73gSelgJBgAJ (there was disagreement about the best data structure for the actual use case)

That's all I found.

Best regards,
Oliver

José Valim

unread,
Mar 20, 2024, 7:44:08 AMMar 20
to elixir-l...@googlegroups.com
Thank you. It seems the major objection was "waiting for a use case".

There is a question about: what happens if you try to update an element that does not exist. But the behaviour in the PR mirrors `at`. We may want to introduce `find!` in the future to mirror `at!` as well.

oliver....@googlemail.com

unread,
Mar 20, 2024, 8:29:22 AMMar 20
to elixir-lang-core
Hello, Jose.

Thank you. :)

I was thinking about adding an Access.find!/1, but since there's no equivalent Enum.find!/2 I stopped thinking about it. But yes, in deep updates it might make more sense than in the Enum.find/2 case.

Best regards,
Oliver

Reply all
Reply to author
Forward
0 new messages