Ruby: Enumerator.newではsizeを指定できる(翻訳)
RubyのあらゆるEnumeratorはcount
メソッドをサポートしています。これは、全項目をイテレートして総数を返します。
irb> enum = Enumerator.new { |yielder|
(1..100).each do |i|
puts "counting item: #{i}"
yielder << i
end
}
irb> enum.count
counting item: 1
counting item: 2
...
counting item: 100
=> 100
しかしEnumerableにはsize
メソッドもあります。違う点は、デフォルトがnil
であることです。
irb> enum.size
=> nil
Rubyでほとんど知られていない機能のひとつが、以下のようにEnumerator.new
にパラメータを渡すことでsize
の問い合わせに対するショートカット的な「回答」を与えられることです。
irb> enum = Enumerator.new(100) { |yielder|
(1..100).each do |i|
puts "counting item: #{i}"
yielder << i
end
}
irb> enum.size
=> 100
もう個数を得るためにイテレーションする必要はありません。
しかしEnumerator.new
には実はもうひとつ知られていない機能があるのです。lambdaを渡すことでサイズの決定を遅延し、しかもイテレートするよりも高速です。
たとえば、何らかのEコマースAPIで製品を以下のようにEnumerator
で処理したいとします。
irb> api = EcommerceApi.new('connection config')
irb> enum = Enumerator.new { |yielder|
api.products.each.with_index do |product, index|
puts "fetching product: #{index}"
yielder << product
end
}
irb> enum.count
fetching product 0
fetching product 1
...
fetching product 235
=> 236
たとえば、そのAPIにtotal_count
エンドポイントがあり、これを使うと効果的に個数を取得できるとします。
irb> api = EcommerceApi.new('connection config')
irb> enum = Enumerator.new(api.products.total_count) { |yielder|
api.products.each.with_index do |product, index|
puts "fetching product: #{index}"
yielder << product
end
}
irb> enum.size
=> 236
もう総数を得るために製品をイテレートする必要はありません。しかし1つ新たな問題が生じます。enum
がsize
を一度も呼び出していない場合であってもtotal_count
が常に実行されてしまうのです。これはいかにも無駄な感じです。さらに、APIに製品が追加されてもこのサイズは変更されません。
以下のようにlambdaを使えば、リクエストがあった場合にのみAPI呼び出しを実行し、しかも常に最新の個数を得られます。
irb> api = EcommerceApi.new('connection config')
irb> enum = Enumerator.new(-> { api.products.total_count }) { |yielder|
api.products.each.with_index do |product, index|
puts "fetching product: #{index}"
yielder << product
end
}
irb> enum.size # Calls -> { api.products.total_count } lambda.
=> 236
この機能は、enum_for
やto_enum
でEnumeratorを作成するときにも利用できます。その場合、enum_for
に渡されたブロックからそれを返す必要があります。このブロック引数は、enum_for
に渡される任意の追加引数です。
irb> def each_number(max = 100)
return enum_for(__method__, max) { |max| max } unless block_given?
(1..max).each { |n| yield n }
end
irb> each_number(200).size
=> 200
追伸: Rubyのこの機能がどう振る舞うかをつい忘れてしまうのですが、ググってもすぐ使える例が見つかったためしがありませんでした。皆さんも本記事でこの機能をすぐ思い出せるようになることを願っています。
概要
原著者の許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。
参考: class
Enumerator
(Ruby 3.2 リファレンスマニュアル)