-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Enum refactoring #5818
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enum refactoring #5818
Conversation
|
@michalmuskala did you, after style guide tweet, decide to start spying on me? 🤔
I think it's better to have common prefix. 👍 |
@lexmag you mean |
I initially wanted to go for |
I prefer |
@whatyouhide by saying common prefix I meant prefix of helper function and public function, for example |
okay, that sounds good :). I agree, let's have |
Ping @michalmuskala? :) |
@whatyouhide maybe we should go ahead and merge it and do the changes ourselves as this PR can easily get stale. |
I tried benchmarking the change to see if it really gives the expected benefits and saw some surprising answers - for |
@michalmuskala I have seen the same numbers in the past, except I never noticed a drastic slow down for 100_000 elements. I would expect some slow downs for certain elements for maps, due to internal resizing, but not for lists. We should probably merge the PR only with the renaming and keep operations tail recursive. Especially because Enumerable.reduce is body recursive, so it would probably be best to keep the same properties. |
Ping. |
313f38f
to
25708a6
Compare
I updated the namings to use Additionally, there are two new commits:
As to the body-recursive vs tail-recursive. We already have some functions body recursive, most notably
|
The reasoning is the same as in elixir-lang#3811 and 99e0d8e
1a3287a
to
14a2da3
Compare
Why is |
Elixir's for handles all enumerables, not only lists. Additionally, there was a guard erroneously removed from |
lib/elixir/lib/enum.ex
Outdated
case Enumerable.count(enumerable) do | ||
{:error, _module} -> | ||
module = Enumerable.impl_for!(enumerable) | ||
case module.count(enumerable) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change is backwards incompatible. It is ok for a module to return ANOTHER module which has the proper implementation. This change also defeats the whole point of returning {:error, __MODULE__}
, which is to avoid two protocol dispatches.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First, the documentation of the enumerable protocol talks about returning {:error, __MODULE__}
only. To me, this means returning anything else than the protocol module is not allowed.
Second, without this, we are doing double dispatch:
- when
Enumerable.count/1
does a dispatch and call returns{:ok, count}
. - in
fetch_enumerable/3
we passEnumerable
fetch_enumerable/3
does another dispatch.
The only time a double-dispatch does not happen in this function is when count returns {:error, __MODULE__}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@michalmuskala we do a double dispatch on the worst case scenario only. You do a double dispatch on all cases. If an implementation doesn't want to perform a double dispatch, then they can always implement count
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@michalmuskala please revert the changes and we will discuss how to proceed regarding count and member? separately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I will revert.
Could you explain where the second dispatch happens in my implementation, though? The first is during impl_for!
call, but I can't find the second one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right. There is no double protocol dispatch on this version of the code. Apologies. We should stick with the current version.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And by current version I mean your version.
lib/elixir/lib/enum.ex
Outdated
case Enumerable.count(enumerable) do | ||
{:error, module} -> | ||
module = Enumerable.impl_for!(enumerable) | ||
case module.count(enumerable) do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here, please revert.
Thanks @michalmuskala, I have added two final comments. |
01dbf2c
to
536e5d2
Compare
----- With input Big (10 Million) ----- Name ips average deviation median tail 1.64 610.99 ms ±13.78% 577.63 ms body 1.48 675.90 ms ±10.06% 681.08 ms for 1.46 687.19 ms ±12.84% 693.82 ms Comparison: tail 1.64 body 1.48 - 1.11x slower for 1.46 - 1.12x slower ----- With input Middle (100 Thousand) ----- Name ips average deviation median tail 201.60 4.96 ms ±15.12% 4.81 ms body 199.52 5.01 ms ±14.03% 4.76 ms for 178.95 5.59 ms ±14.50% 5.39 ms Comparison: tail 201.60 body 199.52 - 1.01x slower for 178.95 - 1.13x slower ---- With input Small (1 Thousand) ----- Name ips average deviation median body 23.98 K 41.70 μs ±38.90% 38.00 μs tail 21.35 K 46.84 μs ±35.64% 44.00 μs for 18.64 K 53.63 μs ±31.60% 50.00 μs Comparison: body 23.98 K tail 21.35 K - 1.12x slower for 18.64 K - 1.29x slower
It was used for dispatch between the new and deprecated version. The guard was erroneously removed in elixir-lang@99e44a1#diff-6881431a92cd4e3ea0de82bf2338f8eaL1032
536e5d2
to
0bd9ead
Compare
I reverted the fetch change, and will open a separate PR. |
@@ -806,7 +806,7 @@ defmodule Enum do | |||
""" | |||
@spec filter(t, (element -> as_boolean(term))) :: list | |||
def filter(enumerable, fun) when is_list(enumerable) do | |||
for item <- enumerable, fun.(item), do: item | |||
filter_list(enumerable, fun) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume we can use :lists.filter/2
here, instead of defining extra function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ignore me, we need to handle falsey values.
Great job @michalmuskala. 💛 |
The reasoning is the same as in Optimize Enum.map/2 for lists #3811 and 99e0d8e