Project

General

Profile

Actions

Feature #17333

open

Enumerable#many?

Added by okuramasafumi (Masafumi OKURA) 9 months ago. Updated 8 months ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:101351]

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) 9 months 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 knu (Akinori MUSHA) 9 months ago

ITYM > 1. 😉

Actions #4

Updated by okuramasafumi (Masafumi OKURA) 9 months ago

  • Description updated (diff)
Actions #5

Updated by okuramasafumi (Masafumi OKURA) 9 months ago

  • Description updated (diff)

Updated by sawa (Tsuyoshi Sawada) 9 months ago

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.

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) 9 months 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&regexp=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&regexp=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.

Actions #8

Updated by okuramasafumi (Masafumi OKURA) 9 months ago

  • Description updated (diff)

Updated by okuramasafumi (Masafumi OKURA) 8 months 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) 8 months 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) 8 months 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.)

Actions

Also available in: Atom PDF