こんにちは寺岡です。
Rubyの次期バージョン、Ruby2.0はRuby誕生から20周年となる2013年2月24日にリリースが予定されています。
キーワード引数、Refinements、Module#prependなどの魅力的な新機能が目白押しなので待ち遠しい限りです。
今回はruby2.0のpreview版を使って、新機能の中でも一押しのEnumerable#lazyメソッドをご紹介したいと思います。
実行環境の準備
まずはRuby2.0の実行環境を準備します。
rvm導入済みの環境に、ruby2.0をインストールします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
$ rvm get head $ rvm get latest $ rvm reload $ rvm list known | grep '\[ruby-\]' [ruby-]1.8.6[-p420] [ruby-]1.8.7[-p371] [ruby-]1.9.1[-p431] [ruby-]1.9.2[-p320] [ruby-]1.9.3-p125 [ruby-]1.9.3-p194 [ruby-]1.9.3-p286 [ruby-]1.9.3-[p327] [ruby-]1.9.3-head [ruby-]2.0.0-preview2 $ rvm install 2.0.0 $ rvm use ruby-2.0.0 $ ruby -v ruby 2.0.0dev (2012-12-01 trunk 38126) [x86_64-linux] |
lazyしてみる
さて、ruby2.0のインストールが終わったところで、本題のEnumerable#lazyが如何に怠惰なメソッドか確認してみたいと思います。
※lazyを直訳すると「怠惰な」という意味になります。
まずはおもむろにirbを起動して、以下のコードを打ち込んでみます。
1 2 3 4 5 |
$ irb > [1,2,3,4,5].select{|n| n.odd?} # normal select => [1, 3, 5] > [1,2,3,4,5].lazy.select{|n| n.odd?}.force # lazy select => [1, 3, 5] |
上が通常のselect、下がlazyを使ったselectです。
lazy,forceというメソッド呼び出しが増えていますね。
これだけでは「呼び出しが増えるなんて、全然怠惰じゃないじゃないか!!」と思われるかもしれません。
どこが怠惰になったのか調べてみることにします。
まずはlazyの戻り値を確認してみます。
1 2 |
> [1,2,3,4,5].lazy => #<Enumerator::Lazy: [1, 2, 3, 4, 5]> |
Enumerator::Lazyクラスのインスタンスだという事がわかりました。
では、selectを呼び出した後はどうなっているのでしょうか。
1 2 |
> [1,2,3,4,5].lazy.select{|n| n.odd?} => #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5]>:select> |
何だこりゃ……。
なんとなく、先ほどのインスタンスがEnumerator::Lazyクラスでラップされている雰囲気を感じます。
Enumerator::Lazyとは何なのでしょうか、まずは親クラスを確認してみたいと思います。
1 2 |
> Enumerator::Lazy.superclass => Enumerator |
Enumeratorということは、eachメソッドが使えそうですね。試してみましょう。
1 2 3 4 5 |
> [1,2,3,4,5].lazy.select{|n| n.odd?}.each{|n| p n} 1 3 5 => nil |
問題無く使えますね。
次はselectに与えるブロックの処理を変更してみます。
と言っても、与えられた引数を表示する処理を追加するだけです。
※余談ですが、pメソッドはRuby1.9から与えた引数をそのまま返してくれるようになったのでとても便利になりました。
1 2 |
> [1,2,3,4,5].lazy.select{|n| p n.odd?} => #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5]>:select> |
おや、pの出力が表示されませんね。
ではeachを呼び出してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 |
> lazy_odd_numbers = [1,2,3,4,5].lazy.select{|n| p n.odd?} => #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5]>:select> > lazy_odd_numbers.each{|n| p n} true 1 false true 3 false true 5 => nil |
selectとeachに与えたブロックが交互に実行されました。
Enumerator::LazyはselectやmapなどのEnumerableモジュールのメソッド実行を、値が必要になるまで遅延してくれるようです。
与えられたタスクに納期ギリギリまで手を付けない。なんて怠惰な奴なのでしょう!!
lazyの怠惰っぷりがわかったところで、どんなメリットがあるのか考えてみます。
省メモリであること
個人的にlazyの一番の利点はこの点だと思っています。
下の呼び出しは結果は同じですが、実行時のメモリ消費量は大きく違います。
1 2 |
> (1..1000).select{|n| n.even?}.map{|n| n.to_s}.each{|s| p s} # normal > (1..1000).lazy.select{|n| n.even?}.map{|n| n.to_s}.each{|s| p s} # lazy |
normalのコードでは、selectを呼び出した段階で1~1000までの偶数すべてを要素とした配列が
メモリ上に確保され、mapを呼び出した段階ではそれを文字列にした配列がメモリ上に確保されます。
メソッドをチェインしていくと、前のメソッドの実行結果(レシーバ) + 今回の実行結果をメモリ上に確保する必要があることになります。
対してlazyのコードでは、select,mapの段階でメモリ上確保されるのは、Enumerator::Lazyのインスタンスだけです。
eachの段階でも、メモリ上に確保されるのは各要素1つに対してselect,mapのブロックを適用した値だけになります。
このように、lazyを利用するとメモリ効率を格段に良くすることが出来ます。
省メモリになったなら、実行速度も早いのでは?と期待してしまいますが、
Enumerator::Lazyでは内部的にFiberを利用していることもあり、大抵の場合実行速度は遅くなってしまいます。
無限リストに適用
Ruby1.9から利用可能なEnumeratorを利用すると簡単に無限リストが作れるようになりました。
今回は、Enumeratorを使ってフィボナッチ数列を求めてみます。
1 2 3 4 5 6 7 |
> fib = Enumerator.new {|yielder| > func = ->(n) { n < 2 ? n : func.(n-2) + func.(n-1)} > i = 0 > loop {yielder << func.(i);i+=1} > } > fib.take(11) # 0~10のフィボナッチ数列 => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55] |
EnumeratorはEnumerableモジュールをincludeしていますので、結果をちょっと加工してみましょう。
1 |
> fib.map{|n| "#{n} is Fibonacci!"}.take(11) |
上のコードを実行しても、いつまでたっても結果が返って来ません。
fibは無限にフィボナッチ数列を返し続けるので、mapの時点で無限ループになってしまうためです。
この例ではtakeとmapの順序を入れ替えれば望み通りの結果が取得できますが、
lazyを使うとこの問題をもっとエレガントに解決することができます。
1 2 |
> fib.lazy.map{|n| "#{n} is Fibonacci!"}.take(11).force => ["0 is Fibonacci!", "1 is Fibonacci!", ...] |
意図した結果が得られました!
無限リストに対してlazyを使うとEnumerableのメソッドで自由自在に加工できるようになります。
おまけ
というわけで、今回はlazyの偉大な怠惰っぷりをちょっとだけご紹介してみました。
「lazyスゲェ!! Ruby1.9で使えたらいいのに……」と思った人居ませんか?
そんな奇特な方向けに、Ruby1.9版lazyを作ってみました。
https://gist.github.com/4201351
Ruby2.0でEnumeratorに追加されたsize、仕様が変更されたeach以外については、
ruby2.0-preview2に付属していたspecが通るように作ったので、そこそこ動作してくれると思います。