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)
.