Ruby 2.0 Lazy Enumerable

最近、私のEnumerable::Lazy



パッチがrubyトランクに採用されました。 これは、ruby 2.0では次のことができることを意味します。

 a = [1,2,3,4,2,5].lazy.map { |x| x * 10 }.select { |x| x > 30 } #=>    a.to_a #=> [40, 50],     .
      
      





なんで?



Rubyは美しく、命令型言語であるにもかかわらず、エレガントな「機能的な」コードを書くことができます。 例:

 data.map(&:split).map(&:reverse)
      
      





それははるかに読みやすいように見えます:

 data.map { |l| l.split.reverse }
      
      





ただし、最初の例は非効率的です。 data



変換するとき、Rubyはデータを2回通過し、中間配列を生成します。 データ量が少ない限り、この欠点は重大ではありません。 ただし、巨大なファイルを解析するとします。

 File.open("text") do |f| f.each.flat_map(&:split).grep(/ruby/) end
      
      





この場合、配列の二重パスは許可されません。 幸いなことに、 Enumerable::Lazy



登場によりEnumerable::Lazy



これは問題ではなくなりました。 Enumerable::Lazy



クラスでオーバーライドされたmap



遅延バージョン、 select



およびその他の多くの関数は、すぐに計算を実行しません。 代わりに、 Enumerable::Lazy



インスタンスを返すため、遅延計算のチェーンを構築できます。



更新する


Enumerator :: Lazyのほとんどの作業が終了したので、実際にどのようなゲインが得られるかをテストすることにしました。 結果は期待はずれでした(初めてベンチマークを間違えたため、下から楽観的な意見が出ました)。 平均して、怠な列挙子は通常の配列の操作よりも4(!)倍遅いです。 これは、遅延チェーンを計算するときに、ブロック呼び出し操作が頻繁に発生するためです。 ruby-lang.orgの議論をご覧ください 。 これにもかかわらず-遅延列挙子の利点は同じです-エレガントで読みやすいコードですが、遅いためにユーザーケースの数が減ります。

遠藤裕介が引用した良い例:

 a = [] Prime.each do |x| next if x % 4 != 3 a << x break if a.size == 10 end
      
      





怠lazを使用すると、次のようになります。

 Prime.lazy.select {|x| x % 4 == 3 }.take(10).to_a
      
      





いつ?



Enumerator



してRubyで遅延コンピューティングを実装する方法を最初に考えたのは誰なのかを言うのは困難です。 おそらく 2008年のこの投稿は最初の投稿の 1つだったでしょう。 実装のアイデアはシンプルで、Enumeratorオブジェクトの注目すべき特性に基づいています-それらはチェーンで接続できます。 それ以来、 多くの 記事公開され 、そのアイデアを説明するコメントがenumerator.cに追加されました。 ruby-langについての議論は3年以上続き、最終的にMatzは Yaya Haraの実装に 投票しました。

Enumerable::lazy



メソッドを提案しました。このメソッドは、配列をEnumerable::Lazy



インスタンスに「ラップ」し、それを使用して遅延チェーンを構築します。 Cのパッチリクエストが発声され、すぐにコールを受け入れました。 ところで、私は最近関数型プログラミングに興味を持ち、Rubyの内部を掘り下げることは非常に興味深いものでした。 その結果、私はpoolリクエストできたことを光栄に思います 。それは、わずかなリファクタリングの後数日前に採用されました 。 トランクで凍結され、Ruby 2.0でリリースされます( ロードマップを参照 )。



どうやって?



列挙子(知っている場合はスキップ)


通常の列挙子が何であり、それが何で食べられるかを知らない人のために:

 enum = [1, 2].each puts enum.next #=> 1 puts enum.next #=> 2 puts enum.next #=> StopIteration exception raised enum = Enumerator.new do |yielder| yielder << 1 yielder << 2 end puts enum.next #=> 1 puts enum.next #=> 2 puts enum.next #=> StopIteration exception raised enum = "xy".enum_for(:each_byte) enum.each { |b| puts b } # => 120 # => 121 o = Object.new def o.each yield yield 'hello' yield [1, 2] end enum = o.to_enum p enum.next #=> nil p enum.next #=> 'hello' p enum.next #=> [1, 2] enum = %w{foo bar baz}.map puts enum.with_index { |w, i| "#{i}:#{w}" } # => ["0:foo", "1:bar", "2:baz"] #     a = [1, 2, 3] some_method(a.enum_for) [1,2,3].cycle.take(10) #=> [1, 2, 3, 1, 2, 3, 1, 2, 3, 1]
      
      





おそらく既に推測したように、 #to_enum



および#enum_for



Kernel



モジュールにあります。つまり、どのオブジェクトからもアクセスできます。 ここにあるtest / ruby​​ / test_enumerator.rbも見ることができる場合、例はenumerator.cから取得されます 。 列挙子の内部構造はおそらく別の投稿に値しますが、 next



このすべての魔法がFiberを使用して実装されていることは注目に値します。



遅延列挙子


Enumerable :: Lazyの仕組みを理解するには、次のコードをご覧ください。

 module Enumerable class Lazy < Enumerator def initialize(obj) super() do |yielder| obj.each do |val| if block_given? yield(yielder, val) else yielder << val end end end end def map Lazy.new(self) do |yielder, val| yielder << yield(val) end end end end a = Enumerable::Lazy.new([1,2,3]) a = a.map { |x| x * 10 }.map { |x| x - 1 } puts a.next #=> 9 puts a.next #=> 19
      
      





これはYutakaによって提案された典型的なRuby実装です。 ただし、この例では、メソッドのパラメーターとして&block



を使用していません(Procに変換してcallをcall



)。 代わりに、 each



のブロック内で直接yield



できeach



block_given?



期待どおりに動作します。 さらに、ブロック内で、 self



呼び出すことができます( return



)。 すごいですね。 Yehuda Katz( )からのこのトピックに関する素晴らしい投稿

コード自体は非常に単純ですが、詳しく見てみましょう。すでに述べたように、主なアイデアはチェーンを構築することです。 したがって、 Enumerable::Lazy#map



は、 Enumerable::Lazy#map



の新しいインスタンスを作成し、引数として「前の」オブジェクトを渡します。 to_a



呼び出されると( next



each



ブロック、 take



など)、計算が実行されます:Rubyはチェーンの先頭に戻り(図1)、元のオブジェクトから次の値を受け取り、それを外側に返し、遅延メソッドのブロックで順次変更します(in彼らが「吊るされた」と同じ順序で)。



図1遅延列挙子のチェーン。



Cパッチ


私のCパッチはRubyの実装を完全に繰り返します。 lazy_initialize



内で直接super



を呼び出す代わりに、ジェネレーター( Enumerator::Generator



)を作成して初期化します。これは、引数として列挙子に渡されます。

最後のパッチでは、 nobuはコードをわずかにリファクタリングしました。ブロック関数内のif-else条件を2つの個別のメソッド( lazy_init_block_i



lazy_init_block



)にlazy_initialize



、条件をlazy_initialize



転送しlazy_initialize





また、Ruby配列をパラメーターとしてブロック関数に渡す代わりに、パラメーターを使用して通常のC配列を構築します。 したがって、現在、 rb_ary_entry



を使用してyielder



とブロック内の値を取得する必要はありません。

たとえば、これ:

 static VALUE lazy_map_func(VALUE val, VALUE m, int argc, VALUE *argv) { VALUE result = rb_funcall(rb_block_proc(), id_call, 1, rb_ary_entry(val, 1)); return rb_funcall(rb_ary_entry(val, 0), id_yield, 1, result); }
      
      





これになった:

 static VALUE lazy_map_func(VALUE val, VALUE m, int argc, VALUE *argv) { VALUE result = rb_yield_values2(argc - 1, &argv[1]); return rb_funcall(argv[0], id_yield, 1, result); }
      
      





率直に言って、私はRuby内部の絶対的な初心者だったので、このかなり些細なプルリクエストを書くのに2週間かかった。 さらに、最初は別の方法を試してみました。レイジーメソッドのブロックを列挙子自体のプロセスとして保存しようとしました。 ここで見る最初のクレイジープルリクエストEnumerator#next



が呼び出されると、すべてのプロセスが次の値に次々と「適用」されました。 怠zyなmap



select



は正常に機能しましたが、 Enumerator#each



を微調整しようとすると、それがどういうわけか複雑すぎる(またはそうでない)ことに気付きました。

あなたはちょうどここにタフになったので、ルーブルで何かにパッチを当てようとするなら 素晴らしい 記事が たくさんあります。 ボーナスとして、なぜ怠laになるべきかについての別の記事があります。



結論



現在、 Enumerator::Lazy



武器にはselect



map



reject



grep



(最初のパッチで追加)、 flat_map



zip



take



take_while



drop



drop_while



cycle



(最近追加されたshugo )があります。 APIの開発と議論は進行中ですが、今すぐ怠けたい場合は、トランクからRubyをコンパイルしてお楽しみください。



注:


PSこれは私の記事の英語からの翻訳です。



All Articles