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をコンパイルしてお楽しみください。
注:
-
Enumerator::Lazy
- ルビの例の
super()
は、そのように呼び出す必要があります-括弧を使用すると、ルビは自動的にすべての引数を渡します
PSこれは私の記事の英語からの翻訳です。