Proposal `Enum.more_than?/2` or `List.more_than?/2`

117 views
Skip to first unread message

Zachary Daniel

unread,
Dec 3, 2020, 12:15:25 PM12/3/20
to elixir-lang-core
Counting a list, especially a large one, to know if there are "more than x" or "less than x" items is inefficient.

Right now I often see things like `if Enum.count(list) > 4 ...`, mostly because writing a recursive `more_than?` check is tedious, or doing something like `Enum.empty?(Enum.drop(list, 4))` is not very expressive.

I think it would be nice to have an `Enum.more_than?` that does that work for you. It could also be `List.more_than?/2` if we don't want it in Enum. Any thoughts?

José Valim

unread,
Dec 3, 2020, 12:21:05 PM12/3/20
to elixir-l...@googlegroups.com
Thanks Zach! I like this idea but the proposed name, for some reason, doesn't sit right with me. Is there any prior art from other langs we could look at?

--
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/263d7c39-a32b-4294-93d8-40f248c9b3c8n%40googlegroups.com.

Zach Daniel

unread,
Dec 3, 2020, 1:32:13 PM12/3/20
to elixir-l...@googlegroups.com
I agree on the name feeling subpar :) I’ll take a look and see if I can find other examples.

Zach Daniel

unread,
Dec 3, 2020, 1:36:03 PM12/3/20
to elixir-l...@googlegroups.com
Nothing is jumping out at me from elsewhere yet, but another option might be accepting options in `Enum.count`, like `Enum.count(list, max: 4)`.  I’ll keep searching though.

Zach Daniel

unread,
Dec 3, 2020, 1:42:59 PM12/3/20
to elixir-l...@googlegroups.com
Another benefit to the options list would be supporting it for count with a predicate, e.g Enum.count(enum, &some_predicate/1, max: 4)

Zach Daniel

unread,
Dec 3, 2020, 5:04:56 PM12/3/20
to elixir-l...@googlegroups.com
This probably off the table/unreasonable, but it also seems like something that could be statically solved and people would never need to know as it is just an optimization. E.g Enum.count(list) > n could optimized by the compiler? Probably wouldn’t be good for all Enums, since counting would be expected to enumerate them, so maybe only something like List.count 🤷‍♂️

Michał Muskała

unread,
Dec 3, 2020, 5:14:27 PM12/3/20
to elixir-l...@googlegroups.com

Unfortunately this can’t be done automatically since it has subtle semantic differences. In particular Enum.count/1 (or length/1) not only traverses the list to count its size, but also verifies it’s a proper list raising an exception for improper lists. The difference could be seen for value like:

 

[1, 2, 3 | :invalid]

 

Calling length/1 or Enum.count/1 on this raises. If compiler did the optimisation you propose, for something like length(list) > 0, it wouldn’t fully traverse the list and wouldn’t raise. Thus such an optimisation is not possible in the general case.

Zach Daniel

unread,
Dec 3, 2020, 6:51:24 PM12/3/20
to elixir-l...@googlegroups.com
Well, List.count doesn’t exist yet, but either way it sounds like not a great idea :) I couldn’t find examples in other Lang’s, so maybe I’ll just throw out some other names:

Enum.at_least?/2

Enum.at_most?/2

Enum.has_count?/2

Allen Madsen

unread,
Dec 3, 2020, 7:06:35 PM12/3/20
to elixir-l...@googlegroups.com
Just throwing out some other names to see if any stick:

Enum.count_above?/2
Enum.count_below?/2
Enum.count_greater?/2
Enum.count_lesser?/2

To put it inline with some of the other compare methods in elixir, like DateTime.compare/2, maybe an API like:

Enum.compare_count([], 1) #=> :lt
Enum.compare_count([1], 1) #=> :eq
Enum.compare_count([1, 2], 1) #=> :gt

eksperimental

unread,
Dec 3, 2020, 7:52:46 PM12/3/20
to elixir-l...@googlegroups.com
On Thu, 3 Dec 2020 19:06:18 -0500
Allen Madsen <allen.c...@gmail.com> wrote:

> Enum.compare_count([], 1) #=> :lt
> Enum.compare_count([1], 1) #=> :eq
> Enum.compare_count([1, 2], 1) #=> :gt

This is the way to go, because in one function call we can determine the
course of the action, such as in

case Enum.compare_count(list, n) do
:lt -> ...
:eq -> ...
:gt -> ...
end

when using the predicate functions it would require at least two
function calls.

>
> Allen Madsen
> http://www.allenmadsen.com
>
>
> On Thu, Dec 3, 2020 at 6:51 PM Zach Daniel
> <zachary....@gmail.com> wrote:
>
> > Well, List.count doesn’t exist yet, but either way it sounds like
> > not a great idea :) I couldn’t find examples in other Lang’s, so
> > maybe I’ll just throw out some other names:
> >
> > Enum.at_least?/2
> >
> > Enum.at_most?/2
> >
> > Enum.has_count?/2
> >
> > On Thu, Dec 3, 2020 at 5:14 PM Michał Muskała <mic...@muskala.eu>
> > wrote:
> >
> >> Unfortunately this can’t be done automatically since it has subtle
> >> semantic differences. In particular Enum.count/1 (or length/1) not
> >> only traverses the list to count its size, but also verifies it’s
> >> a proper list raising an exception for improper lists. The
> >> difference could be seen for value like:
> >>
> >>
> >>
> >> [1, 2, 3 | :invalid]
> >>
> >>
> >>
> >> Calling length/1 or Enum.count/1 on this raises. If compiler did
> >> the optimisation you propose, for something like length(list) > 0,
> >> it wouldn’t fully traverse the list and wouldn’t raise. Thus such
> >> an optimisation is not possible in the general case.
> >>
> >>
> >>
> >> *From: *elixir-l...@googlegroups.com <
> >> elixir-l...@googlegroups.com>
> >> *Date: *Thursday, 3 December 2020 at 22:04
> >> *To: *elixir-l...@googlegroups.com <
> >> elixir-l...@googlegroups.com>
> >> *Subject: *Re: [elixir-core:9802] Proposal `Enum.more_than?/2` or
> >> <https://groups.google.com/d/msgid/elixir-lang-core/263d7c39-a32b-4294-93d8-40f248c9b3c8n%40googlegroups.com?utm_medium=email&utm_source=footer>
> >> .
> >>
> >> --
> >> 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/CAGnRm4JX4NE1yWH1G5L_DjF18v8zejF0%2BSkb_oz%3DPiUHM8Mz1w%40mail.gmail.com
> >> <https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4JX4NE1yWH1G5L_DjF18v8zejF0%2BSkb_oz%3DPiUHM8Mz1w%40mail.gmail.com?utm_medium=email&utm_source=footer>
> >> .
> >>
> >> --
> >> 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/CAK-yb0BBGCrgbZamFs%2BeqLUis6mFQgvUHkKK1htSN5rDDWwMRQ%40mail.gmail.com
> >> <https://groups.google.com/d/msgid/elixir-lang-core/CAK-yb0BBGCrgbZamFs%2BeqLUis6mFQgvUHkKK1htSN5rDDWwMRQ%40mail.gmail.com?utm_medium=email&utm_source=footer>
> >> .
> >>
> >> --
> >> 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/DB7PR07MB3899C92933992464F17898E1FAF20%40DB7PR07MB3899.eurprd07.prod.outlook.com
> >> <https://groups.google.com/d/msgid/elixir-lang-core/DB7PR07MB3899C92933992464F17898E1FAF20%40DB7PR07MB3899.eurprd07.prod.outlook.com?utm_medium=email&utm_source=footer>
> >> .
> >>
> > --
> > 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/CAK-yb0BO2QESHcaL7-svOoAGqvr6hJi%3D8AHFqi-qNZdoFEMMwA%40mail.gmail.com
> > <https://groups.google.com/d/msgid/elixir-lang-core/CAK-yb0BO2QESHcaL7-svOoAGqvr6hJi%3D8AHFqi-qNZdoFEMMwA%40mail.gmail.com?utm_medium=email&utm_source=footer>
> > .
> >
>

Zach Daniel

unread,
Dec 3, 2020, 8:14:37 PM12/3/20
to elixir-l...@googlegroups.com

José Valim

unread,
Dec 4, 2020, 12:18:42 AM12/4/20
to elixir-l...@googlegroups.com
Thanks Allen! I believe that's a good idea.

I think the main insight is that we don't want a predicate function (at_least? more_than?). Using compare returns three states - which is better than two - but what if we just returned the number? After all, if I am interested in knowing if something has less than 10, 10, or more than 10, I just need to count until eleven. Returning a number seems to be more flexible too. Therefore, what do you think about: count_until(enum, value)?

To check if less, eq, or more than 10:

case Enum.count_until(count, 10 + 1) do
  11 -> :gt
  10 -> :eq
  _ -> :lt
end

For at least 10:

Enum.count_until(count, 10) == 10

For more than 10:

Enum.count_until(count, 10 + 1) > 10

Thoughts?


Jayson Vantuyl

unread,
Dec 4, 2020, 2:08:17 AM12/4/20
to elixir-l...@googlegroups.com
There are three questions I don’t think we’re considering:
* What does it mean to “partially count” an Enumerable that implements an “efficient” `count/1` function?
* If such an Enumerable has side-effects for its `reduce/3` function, should they be somehow still happen even though the `count/1` doesn’t necessarily iterate the elements?
* If such an Enumerable returns a larger count that asked for, should we return the larger “technically correct” value; or the `max + 1` value?

I generally like `count_until/2` because it‘s unopinionated about what you’re doing with the count. But the answers to the above question probably should be addressed and documented.

I really see two ways to address the above question. Either we consider “count until implies actively counting” or “count until should take advantage of all optimizations and ignore side-effects”.

My feel is that the latter is generally going to be more efficient in the common case but the former is less likely to create unexpected behavior from people who don’t know how their Enumerable is implemented.

I’m inclined to favor the former. It won’t throw away efficiency that a custom Enumerable will implement, it’ll generally make naive code faster, and the rare cases where people expect side-effects is probably less important than either of those other benefits.

Sent from my iPhone

On Dec 3, 2020, at 21:18, José Valim <jose....@dashbit.co> wrote:



José Valim

unread,
Dec 4, 2020, 2:28:45 AM12/4/20
to elixir-l...@googlegroups.com
That's a very good point Jayson. I think we should go with "count until should take advantage of all optimizations and ignore side-effects”. I believe it is fair to expect that no enumerable that implements count actually has side-effects, exactly because of the implications of what you said.

José Valim

unread,
Dec 4, 2020, 2:29:29 AM12/4/20
to elixir-l...@googlegroups.com
We can also add Enum.count_until(enumerable, filter, n) and you can use filter = & &1 if you want to force enumeration, like there is for Enum.count/2 today.

Zach Daniel

unread,
Dec 4, 2020, 10:23:45 AM12/4/20
to elixir-l...@googlegroups.com
I’ll make a PR with count_until then, and we can see how it looks in code!

mario.luis...@gmail.com

unread,
Dec 4, 2020, 10:50:18 AM12/4/20
to elixir-lang-core
This would be the first function in Enum with "until" in its name. For consistency with the other functions in this module, wouldn't it be preferable the "while" suffix instead ?

Zachary Daniel

unread,
Dec 4, 2020, 11:19:59 AM12/4/20
to elixir-lang-core
`count_while` would imply (to me) that it counts while a predicate returns true. The only name I can think of that would be expressive and consistent with other Enum functions would be something like `Enum.count_take` or `Enum.take_count`, but I think that `Enum.take_until` is a better name.

Adam Lancaster

unread,
Dec 4, 2020, 11:24:06 AM12/4/20
to elixir-l...@googlegroups.com
What about count_upto or count_up_to

This is similar to the ruby method .upto.



Zachary Daniel

unread,
Dec 4, 2020, 11:33:17 AM12/4/20
to elixir-lang-core
I've made a PR here, but I'm happy to change change the name if we land on something better: https://github.com/elixir-lang/elixir/pull/10532/files

I personally still prefer `count_until`.

Jayson Vantuyl

unread,
Jan 11, 2021, 9:17:13 PM1/11/21
to elixir-l...@googlegroups.com
Isn't count_until(x) the same as count_while(not x)?  Unless we're going to make two versions of each one, it really seems to make more sense to stick with one naming and one logic.  It would be useful, too, for situations where we want to refactor a "count_while" into a "take_while".  Aligning the naming and logic makes that a lot clearer.
Jayson Vantuyl
Staff Software Engineer | Infrastructure

405 Howard St Fl 2
San Francisco, CA 94105


Austin Ziegler

unread,
Jan 11, 2021, 9:38:01 PM1/11/21
to elixir-l...@googlegroups.com
Reply all
Reply to author
Forward
0 new messages