Rubyで無限リストを遅延評価
Ruby実践でLazy Enumeratorを使う
Ruby 2.0 以降、Enumerator::Lazy が導入され、Enumerable のメソッドを遅延評価で実行できるようになりました。これにより、巨大な配列やストリームを扱う際にメモリフットプリントを抑えつつ、必要なデータだけを逐次処理できます。
lazy_numbers = (1..Float::INFINITY).lazy
result = lazy_numbers.select { |n| n % 2 == 0 }.first(5)
# => [2, 4, 6, 8, 10]
上記の例では、無限リストを作成し、偶数だけを選択して最初の5つを取得しています。lazy を付けることで、実際に要素が必要になるまで計算が遅延され、メモリ使用量を最小限に抑えます。
Enumerable応用2: 無限リストと遅延評価
無限リストは「終わりのない」データ構造で、実際には必要な部分だけが生成されます。Enumerator::Lazy を組み合わせると、遅延評価が可能になり、巨大データ処理に最適です。
primes = Enumerator.new do |y|
n = 2
loop do
y << n
n += 1
end
end.lazy
first_five_primes = primes.select { |x| x.prime? }.first(5)
# => [2, 3, 5, 7, 11]
このように、select や first などのメソッドは、必要な要素が確定した時点で停止します。遅延評価により、計算コストを大幅に削減できます。
パフォーマンス向上のテクニック
Lazy Enumerator を活用する際のポイントは以下の通りです。
mapやselectのチェーンは遅延評価で実行されるため、途中でfirstやtakeを使うと早期に停止できます。- メモリ使用量を抑えるために、
each_sliceでバッチ処理を行うと、同時に保持する要素数を制限できます。 - 外部データベースやファイルからストリームを読み込む場合は、
Enumerator.newでカスタムイテレータを作成し、必要な時だけデータをフェッチします。
これらのテクニックを組み合わせることで、Ruby での巨大データ処理が格段に高速化します。
巨大データ処理の実践例
実際に 10GB 以上のログファイルを解析するケースを想定します。従来の方法では全行をメモリに読み込むと OOM になることが多いですが、Lazy Enumerator を使えば行単位で処理できます。
log_lines = Enumerator.new do |y|
File.foreach('large_log.txt') { |line| y << line }
end.lazy
error_counts = log_lines
.select { |l| l.include?('ERROR') }
.each_with_object(Hash.new(0)) { |l, h| h[l.split[1]] += 1 }
puts error_counts
このスクリプトは、ファイルを一行ずつ読み込み、ERROR を含む行だけを集計します。全行を一度に保持しないため、メモリ使用量は数 MB に抑えられ、処理時間も大幅に短縮されます。
コメント
コメントを投稿