Feature #17333
openEnumerable#many?
Added by okuramasafumi (Masafumi OKURA) about 4 years ago. Updated almost 4 years ago.
Description
Enumerable#many?
method is implemented in ActiveSupport.
https://api.rubyonrails.org/classes/Enumerable.html#method-i-many-3F
However, it's slightly different from Ruby's core methods such as one?
or all?
, where they take pattern argument.
I believe these methods should behave the same so that it's easier to guess and learn.
We already have none?
, one?
, any?
and all?
, which translate into == 0
, == 1
, > 0
and == self.size
.
many?
method translates into > 1
, which is reasonable to exist.
Currently we need to write something this:
[1, 2, 3].count(&:odd?) > 1
With many?
, we can make it simpler:
[1, 2, 3].many?(&:odd?)
Pull Request on GitHub is available at https://github.com/ruby/ruby/pull/3785
Updated by okuramasafumi (Masafumi OKURA) about 4 years ago
okuramasafumi (Masafumi OKURA) wrote:
Currently we need to write something this:
[1, 2, 3].count(&:odd?).size >= 1
That's my mistake, we can currently do
[1, 2, 3].count(&:odd?) >= 1
Updated by okuramasafumi (Masafumi OKURA) about 4 years ago
Pull Request is here:
https://github.com/ruby/ruby/pull/3785
Updated by knu (Akinori MUSHA) about 4 years ago
ITYM > 1
. 😉
Updated by okuramasafumi (Masafumi OKURA) about 4 years ago
- Description updated (diff)
Updated by okuramasafumi (Masafumi OKURA) about 4 years ago
- Description updated (diff)
Updated by sawa (Tsuyoshi Sawada) about 4 years ago
We already have
none?
,one?
,any?
andall?
, which translate into== 0
,== 1
,> 0
and== self.size
.
many?
method translates into> 1
, which is reasonable to exist.
I do not follow this argument.
Of the methods you have mentioned, any?
and all?
have the strongest reason to exist in Ruby as they are two of the three basic quantifiers/operators of quantificational logic: ¬ (not), ∃ (some; any as in Is there anyone?), and ∀ (all; any as in Any programmer is lazy). none?
has a bit weaker motivation, but is reasonable as it is simply a combination of them: ¬∃. The next quantifier that would be reasonable to exist in Ruby would correspond to the combination: ¬∀ (not all) (But note that I am not claiming here that such method should actually exist). These quantifiers can be paraphrased as:
-
any?
:foo_1 || foo_2 || ... || foo_n
-
all?
:foo_1 && foo_2 && ... && foo_n
-
none?
:!(foo_1 || foo_2 || ... || foo_n)
- not all:
!(foo_1 && foo_2 && ... foo_n)
Compared to them, one?
has weaker motivation to exist (under the semantics it is given) in Ruby as it is not easy to express it in the above way and is a much more complicated notion. So it is justified by having enough use cases.
Now, many?
has at most as less motivation as one?
has. It must be backed up by use cases. What are its use cases?
Updated by okuramasafumi (Masafumi OKURA) about 4 years ago
Now, many? has at most as less motivation as one? has. It must be backed up by use cases. What are its use cases?
I agree. So here are some insights.
https://grep.app/search?q=%5C.many%5C%3F®exp=true&filter[lang][0]=Ruby&filter[lang][1]=HTML%2BERB
This link shows there are more than 100 usages of Enumerable#many?
from ActiveSupport
on GitHub.
Although not all of them is actual use cases (some are documentation or test), some gem authors already use many?
.
https://grep.app/search?q=%5C.count%20%5C%7B.%2A%5C%7D%20%5C%3E®exp=true&filter[lang][0]=Ruby&filter[lang][1]=HTML%2BERB
The link above shows some developers use count {} > 1
, which will be replaced by many?
method.
Updated by okuramasafumi (Masafumi OKURA) about 4 years ago
- Description updated (diff)
Updated by okuramasafumi (Masafumi OKURA) almost 4 years ago
Here are a usecase where we could use many?
over count
for better performance.
https://github.com/Homebrew/brew/blob/master/Library/Homebrew/cask/audit.rb#L188
This code, cask.artifacts.count { |k| k.is_a?(Artifact::Uninstall) } > 1
calls block as many times as the size of cask.artifact
. In contrast, cask.artifacts.many? { |k| k.is_a?(Artifact::Uninstall) }
calls blocks only before finding second matching artifact, which could improve performance.
Like this case where we cannot expect the size of collection, using count
could cost a lot and introducing many?
could help.
Updated by Dan0042 (Daniel DeLorme) almost 4 years ago
That artifacts.count
code was a bad example; since this is error checking, in the normal case you will check all elements and pass. Only in the failure case would you avoid checking all elements, and at that point this kind of performance optimization is of no concern.
Like sawa I feel that many?
in itself is too specific, not useful enough to be worth adding in core. I could sort-of imagine making this a special case of one?
, where 0 items is nil and > 1 is false (so many?
is one? == false
). Or if we could pass a block to first
then we could have first(2){ condition }.size > 1
in order to be efficient. Or by adding a max
keyword parameter to enumerable operations such as select/reject/count, we could do count(max: 2){ condition } > 1
. Imho these are all more generic and better solutions than this overly specific many?
Updated by austin (Austin Ziegler) almost 4 years ago
Like Daniel in #note-10 and Sawa in #note-6, I don’t think that this is a great choice, although many?
is surprisingly complex to implement efficiently. The simplest Ruby implementation I could come up with is this:
def many?
reduce(false) { |f, v|
vp = block_given? ? yield v : v
break true if f && vp
vp
}
end
It’s probably a little less efficient than the ActiveSupport extension (which uses two different branches for block_given?
or not). Something similar was recently suggested to the Elixir core list, and what was decided there is I think a little more generalizable, count_until
(https://github.com/elixir-lang/elixir/pull/10532).
Here’s a Ruby implementation of what could be Enumerable#count_until?
def count_until(limit, match = nil, &block)
cnt = 0
if match
warn 'warning: given block not used' if block
block = ->(v) { v == match }
elsif !block
return [limit, count].min if respond_to?(:size)
block = ->(_) { true }
end
stop_at = limit - 1
reduce(0) { |c, v|
c += 1 if block.(v)
break limit if c == stop_at
c
}
end
# (1..20).count_until(5) # => 5
# (1..20).count_until(50) # => 20
# (1..10).count_until(10) == 10 # => true # At least 10
# (1..11).count_until(10) == 10 # => true # At least 10
# (1..11).count_until(10 + 1) > 10 # => true # More than 10
# (1..5).count_until(10) < 10 # => true # Less than 10
# (1..10).count_until(10 + 1) == 10 # => true # Exactly ten
This could be easily implemented as count(until: 10)
or count(2, until: 10)
or count(match: 2, until: 10)
instead of a different method entirely.
(Sorry if this ends up showing up twice; I sent it first by email, but it appears that my email never made it.)