Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby: Enumerator.newにはsizeを指定できる(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。

参考: class Enumerator (Ruby 3.2 リファレンスマニュアル)

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つ新たな問題が生じます。enumsizeを一度も呼び出していない場合であっても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_forto_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のこの機能がどう振る舞うかをつい忘れてしまうのですが、ググってもすぐ使える例が見つかったためしがありませんでした。皆さんも本記事でこの機能をすぐ思い出せるようになることを願っています。

関連記事

Ruby: Enumerableをreduceで徹底理解する#1 基本編(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。