😊

[Feature #21135] 全要素ループせずに count を比較できるようにする提案

2025/02/17に公開

[Feature #21135] Feature request: Enumerable.compare_count

  • Enumerable#count#each の処理に依存しているので次のように #each の処理が遅いと #count もその分遅くなります
# lazy 化することで each 毎に map のブロックが呼ばれる
d = (1..10).lazy.map { sleep 0.2 }

# 0.2s x 10 = 2.0s 時間がかかる
pp d.count
  • なので d.count >= 3 のような比較をすると #count の処理分だけ遅くなっています
  • しかし、実際には最後までループしなくても『3回ループした時点で3以上である』という判定自体は可能です
  • と、いうこと行う Enumerable#compare_count を追加する提案です
  • チケットで提示されている Ruby の実装は以下のような感じ
module Enumerable
  def compare_count(operation, standard, &blk)
    case operation
    when :<
      compare_count_lt(standard, &blk)
    when :>=
      !compare_count_lt(standard, &blk)
    when :>
      compare_count_spaceship(standard, &blk) == 1
    when :<=
      compare_count_spaceship(standard, &blk) != 1
    when :==
      compare_count_spaceship(standard, &blk) == 0
    when :!=
      compare_count_spaceship(standard, &blk) != 0
    end
  end

  def compare_count_lt(standard, &blk)
    truncate_count(standard - 1, &blk) < standard
  end

  def compare_count_spaceship(standard, &blk)
    truncate_count(standard, &blk) <=> standard
  end

  def truncate_count(limit, &blk)
    return 0 if limit < 0
    found = 0
    each do |ele|
      found += 1 unless blk && !yield(ele)
      break if found > limit
    end
    found
  end
end

# lazy 化することで each 毎に map のブロックが呼ばれる
d = (1..10).lazy.map { sleep 0.2 }

# 元のコードは 2.0s 時間がかかっていたがこれだと 0.6s で処理される
pp d.compare_count(:>=, 3)
# => true

# 逆に比較対象が count より多い場合はその分ループする必要はあるので
# これは 2.0s 時間がかかる
pp d.compare_count(:>=, 30)
# => false
  • パッと具体的な例は出てこないんですがループする事自体にコストがかかるようなオブジェクトだと結構有効そうではあるんですかね?
    • 例えば IO に依存するような読み込み処理に対して #each するようなオブジェクトとか
  • これなんですが似たような遅延処理としてすでに Enumerator::Lazy が存在しているのでそれで拡張できないか、みたいなコメントがされています
class Enumerator
  class Lazy
    class Length
      def initialize(enum)
        @enum = enum
      end

      def force = @enum.to_a.length
      def <=>(value) = force <=> value

      def <(value) = value >= 0 && @enum.take(value).to_a.length < value
      def <=(value) = value >= 0 && @enum.take(value + 1).to_a.length <= value
      def >(value) = value <= 0 || @enum.take(value + 1).to_a.length > value
      def >=(value) = value <= 0 || @enum.take(value).to_a.length == value
      def ==(value) = value >= 0 && @enum.take(value + 1).to_a.length == value
      def !=(value) = value < 0 || @enum.take(value + 1).to_a.length != value
    end

    def length = Length.new(self)
  end
end

# lazy 化することで each 毎に map のブロックが呼ばれる
d = (1..10).lazy.map { sleep 0.2 }

# lazy.length 経由して長さを遅延して判定する
pp d.lazy.length >= 3
# => true

pp d.lazy.length >= 30
# => false
  • これはきれいな実装ですねー
  • チケットの起票者もこれを元にして別の解決策を検討してみるということでこのチケット自体は閉じられています
GitHubで編集を提案

Discussion