Changxin

Changxin

How to `get` or `remove` the first matched element in a list without traverse it?

Hi, are there some functions like

Enum.find_first(enumerable, x -> boolen())
# or
Enum.fliter_first(enumerable, func)
Enum.remove_first(enumerable, func)

I don’t want to traverse a whole list, but some implements like below are not so efficent.

def filter_first([], _func), do: {nil, []}
def filter_first([h | t], func) do
  case func.(h) do
    true -> {h, t}
    false ->
      {first, left} = filter_first(t, func)
      {first, [h | left]}
  end
end

are there some efficient ways to make it?

Marked As Solved

hst337

hst337

The most efficient and simple solution is to use pathex

To define first element in an enumerable which satisfies the condition (for which func returns true), you can write something like

import Pathex; import Pathex.Lenses
path_to_first = some() ~> filtering(func)

And you can use this path to delete, set, update, etc. using Pathex verbs like

Pathex.delete(enumerable, path_to_first)
Pathex.set(enumerable, path_to_first, new_value)
Pathex.over(enumerable, path_to_first, fn old_value -> old_value + 2 end)

Also Liked

sabiwara

sabiwara

Elixir Core Team

Interesting trick. I think you meant body recursion :slight_smile:

Just for the sake of completion, here is the tail recursive version:


  def find_delete(list, fun) when is_function(fun, 1) do
    do_find_delete(list, fun, [])
  end

  defp do_find_delete([head | tail], fun, acc) do
    if fun.(head) do
      {head, :lists.reverse(acc, tail)}
    else
      do_find_delete(tail, fun, [head | acc])
    end
  end

  defp do_find_delete([], _fun, acc) do
    {nil, :lists.reverse(acc)}
  end

This rough benchmark gives me a slightly faster time for the tail-recursive version, but takes slightly more memory (results). Of course this would vary based on OS, list size, OTP version…

kokolegorille

kokolegorille

Hello and welcome,

You could use Enum.find, as it returns the first match…

al2o3cr

al2o3cr

Using a function that returns a list on non-list Enumerables is possible, but it’s going to 100% have to traverse the whole structure to create all the list cells.

For instance:

  def filter_first(enum, func) do
    case Enum.reduce(enum, {:not_found, []}, &reduce_filter_first(&1, &2, func)) do
      {:ok, result, list} ->
        {result, Enum.reverse(list)}

      {:not_found, list} ->
        {nil, Enum.reverse(list)}
    end
  end

  defp reduce_filter_first(el, {:ok, result, rest}, _func) do
    {:ok, result, [el | rest]}
  end

  defp reduce_filter_first(el, {:not_found, rest}, func) do
    if func.(el) do
      {:ok, el, rest}
    else
      {:not_found, [el | rest]}
    end
  end

This uses Enum.reduce to accept anything that speaks the Enumerable protocol.

However, it has a couple of efficiency concerns:

  • has to traverse every element of the input list, even if the value has already been found since the result from reduce contains the list in reverse order
  • returns a new list even if the input was a list and the function never returned true. (for the same reason)
  • it uses less stack, but creates more garbage on the heap (the entire input of Enum.reverse will become garbage)

The situation on the BEAM is more complicated than “body recursion BAD, tail recursion GOOD”.

See a recent post where I went much farther down that rabbithole than I expected to:


BUT TO REITERATE

If you have enough elements in the list for any of this to really matter, you should be looking for a better data structure - one that supports deletions from arbitrary positions faster than O(length).

al2o3cr

al2o3cr

Can you describe what you’re concerned about with regards to efficiency with that code?

One minor issue I see is that filter_first with a predicate that returns false for every element in the list will create a new list despite not actually removing anything. You could avoid that by distinguishing “nil was found in the list” from “nothing was found in the list”:

  def filter_first(list, func) do
    case do_filter_first(list, func) do
      {first, rest} ->
        {first, rest}

      :not_found ->
        {nil, list}
    end
  end

  def do_filter_first([], _func), do: :not_found
  def do_filter_first([h | t], func) do
    if func.(h) do
      {h, t}
    else
      case do_filter_first(t, func) do
        {first, rest} ->
          {first, [h | rest]}

        :not_found ->
          :not_found
      end
    end
  end

(if you squint just a little, you can see a Maybe monad happening here)

HOWEVER

If you have code that’s regularly removing elements from the middle of very long lists - the sort of lists where this optimization might be useful - consider picking a better data structure to support that operation.

eksperimental

eksperimental

Sounds like a problem for Enum.reduce_while/3

Where Next?

Popular in Questions Top

Fl4m3Ph03n1x
Background Let’s assume I have a typical GenServer that receives messages as requests, does some operation in a DB and returns responses....
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
chensan
I have a User schema with a :from_id field set to type :string: defmodule TweetBot.Repo.Migrations.CreateUsers do use Ecto.Migration ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
kostonstyle
Hi all I want to have a unix time, from the current time plus 1 hour. DateTime.now + 1 hour How to get it in elixir? Thanks
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> someth...
New
sabri
Can someone explain the settings of pool_size of Ecto in config file? and what is the recommend size? Thanks
New
Mooodi
Given a string, how can I get access to its character by index? Enum.at("my_string", 2) doesn't work. Or rather, not char, but a substr...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New

Other popular topics Top

lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1140 51847 244
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
977 41022 311
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
sergio
I couldn’t find any guides that worked well with Phoenix 1.6.0 and esbuild. I hope this helps people test the waters and eases you into t...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New

We're in Beta

About us Mission Statement