😊
[Feature #21135] 全要素ループせずに count を比較できるようにする提案
[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
するようなオブジェクトとか
- 例えば IO に依存するような読み込み処理に対して
- これなんですが似たような遅延処理としてすでに
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
- これはきれいな実装ですねー
- チケットの起票者もこれを元にして別の解決策を検討してみるということでこのチケット自体は閉じられています
Discussion