なぜ繊維が必要なのですか


100

ファイバーの場合、古典的な例:フィボナッチ数列の生成

fib = Fiber.new do  
  x, y = 0, 1 
  loop do  
    Fiber.yield y 
    x,y = y,x+y 
  end 
end

ここにファイバーが必要なのはなぜですか?これを同じProcで書き換えることができます(実際には、クロージャー)。

def clsr
  x, y = 0, 1
  Proc.new do
    x, y = y, x + y
    x
  end
end

そう

10.times { puts fib.resume }

そして

prc = clsr 
10.times { puts prc.call }

同じ結果を返します。

だから繊維の利点は何ですか。ラムダや他のクールなRuby機能ではできない、Fibresでどんな種類のものを書けますか?


4
古いフィボナッチの例では、ちょうど最悪の動機である;-)あなたが計算に使用することができさえ式がある任意の O(1)におけるフィボナッチ数は。
usr

17
問題はアルゴリズムではなく、ファイバーの理解に関するものです:)
fl00r 2012年

回答:


229

ファイバーは、アプリケーションレベルのコードで直接使用することはおそらくないでしょう。これらはフロー制御プリミティブであり、これを使用して他の抽象化を構築し、それを上位レベルのコードで使用できます。

おそらく、Rubyでのファイバーの最大の使用法はEnumerator、Ruby 1.9のコアRubyクラスであるを実装することです。これらは非常に便利です。

Ruby 1.9では、ブロック渡さずにコアクラスのほとんどすべての反復子メソッドを呼び出すと、が返されますEnumerator

irb(main):001:0> [1,2,3].reverse_each
=> #<Enumerator: [1, 2, 3]:reverse_each>
irb(main):002:0> "abc".chars
=> #<Enumerator: "abc":chars>
irb(main):003:0> 1.upto(10)
=> #<Enumerator: 1:upto(10)>

これらEnumeratorのはEnumerableオブジェクトであり、それらのeachメソッドは、ブロックで呼び出された場合、元のイテレーターメソッドによって生成されたはずの要素を生成します。先ほどの例では、によって返されるEnumeratorにreverse_eachは、each3、2、1を生成するメソッドがあります。によって返されるchars列挙子は "c"、 "b"、 "a"(など)を生成します。ただし、元のイテレータメソッドとは異なり、列挙子は、next繰り返し呼び出すと、要素を1つずつ返すこともできます。

irb(main):001:0> e = "abc".chars
=> #<Enumerator: "abc":chars>
irb(main):002:0> e.next
=> "a"
irb(main):003:0> e.next
=> "b"
irb(main):004:0> e.next
=> "c"

「内部イテレーター」と「外部イテレーター」について聞いたことがあるかもしれません(「Gang of Four」デザインパターンの本に両方の説明が記載されています)。上記の例は、列挙子を使用して内部イテレーターを外部イテレーターに変換できることを示しています。

これは、独自の列挙子を作成する1つの方法です。

class SomeClass
  def an_iterator
    # note the 'return enum_for...' pattern; it's very useful
    # enum_for is an Object method
    # so even for iterators which don't return an Enumerator when called
    #   with no block, you can easily get one by calling 'enum_for'
    return enum_for(:an_iterator) if not block_given?
    yield 1
    yield 2
    yield 3
  end
end

試してみよう:

e = SomeClass.new.an_iterator
e.next  # => 1
e.next  # => 2
e.next  # => 3

ちょっと待って...何か奇妙に見えますか?あなたは書いたyield中で文をan_iterator直線的コードとしてではなく、列挙子は、それらを実行することができ一つずつ。の呼び出しの合間にnext、の実行an_iteratorは「凍結」されます。を呼び出すたびnextに、次のyieldステートメントまで実行され続け、再び「フリーズ」します。

これがどのように実装されているか推測できますか?Enumeratorはへの呼び出しをan_iteratorファイバーでラップし、ファイバーを中断するブロックを渡します。したがってan_iterator、ブロックに譲るたびに、それが実行されているファイバーは一時停止され、実行はメインスレッドで続行されます。次にを呼び出すとnext、制御がファイバーに渡され、ブロックは戻りan_iterator中断したところから続行します。

繊維なしでこれを行うには何が必要かを考えると参考になります。内部イテレータと外部イテレータの両方を提供したいすべてのクラスには、への呼び出し間の状態を追跡するための明示的なコードを含める必要がありますnext。nextを呼び出すたびに、その状態を確認し、値を返す前に更新する必要があります。ファイバーを使用すると、内部イテレーターを外部イテレーターに自動的に変換できます

これはファイバーパーセーとは関係ありませんが、列挙子で実行できるもう1つのことを述べます。これにより、高次列挙型メソッドを以外の他の反復子に適用できますeach。それについて考える:通常、すべての列挙メソッドは、含むmapselectinclude?inject、というように、すべての要素に関する作業はにより得られましたeach。しかし、オブジェクトに他のイテレータがある場合はどうなりeachますか?

irb(main):001:0> "Hello".chars.select { |c| c =~ /[A-Z]/ }
=> ["H"]
irb(main):002:0> "Hello".bytes.sort
=> [72, 101, 108, 108, 111]

ブロックなしでイテレータを呼び出すと、列挙子が返され、その上で他のEnumerableメソッドを呼び出すことができます。

繊維に戻ってtake、Enumerable のメソッドを使用しましたか?

class InfiniteSeries
  include Enumerable
  def each
    i = 0
    loop { yield(i += 1) }
  end
end

何かがそのeachメソッドを呼び出す場合、それは決して戻るべきではないように見えますよね?これをチェックしてください:

InfiniteSeries.new.take(10) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

これがフードの下で繊維を使用するかどうかはわかりませんが、可能です。ファイバーを使用して、シリーズの無限リストと遅延評価を実装できます。列挙子で定義されたいくつかの遅延メソッドの例として、ここでいくつか定義しました:https : //github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb

ファイバーを使用して汎用コルーチン設備を構築することもできます。私は自分のプログラムでコルーチンを使用したことがありませんが、知っておくとよい概念です。

これが可能性についての考えを与えてくれることを願っています。最初に述べたように、ファイバーは低レベルのフロー制御プリミティブです。これにより、プログラム内で複数の制御フローの「位置」(本のページの異なる「ブックマーク」など)を維持し、必要に応じてそれらを切り替えることができます。任意のコードがファイバーで実行できるので、ファイバーでサードパーティのコードを呼び出し、それを「フリーズ」して、制御するコードにコールバックしたときに何か他のことを続けることができます。

このようなものを想像してみてください:あなたは多くのクライアントにサービスを提供するサーバープログラムを書いています。クライアントとの完全な対話には一連のステップが含まれますが、各接続は一時的なものであり、接続間の各クライアントの状態を覚えておく必要があります。(Webプログラミングのように聞こえますか?)

その状態を明示的に保存し、クライアントが接続するたびに状態をチェックする(クライアントが実行する必要のある次の「ステップ」を確認する)のではなく、各クライアントのファイバーを維持できます。クライアントを識別したら、ファイバーを取得して再起動します。次に、各接続の最後に、ファイバーを一時停止して再び保管します。このようにして、直線的なコードを記述して、すべての手順を含む完全な対話のためのすべてのロジックを実装できます(プログラムがローカルで実行されるようにした場合と同様に)。

(少なくとも今のところ)そのようなことが実用的でない理由はたくさんあると思いますが、ここでも、いくつかの可能性を紹介しようとしています。知るか; コンセプトを理解したら、まだ誰も考えていないまったく新しいアプリケーションを思いつくかもしれません!


回答ありがとうございます!それで、なぜ彼らはcharsクロージャーだけの実装や他の列挙子を実装しないのですか?
fl00r

@ fl00r、さらに情報を追加することを考えていますが、この回答がすでに長すぎるかどうかはわかりません...もっと知りたいですか?
Alex D

13
この回答は非常に良いので、どこかにブログ投稿として書く必要があります。
Jason Voegele

1
更新:EnumerableRuby 2.0にはいくつかの「遅延」メソッドが含まれるようです。
Alex D

2
takeファイバーは必要ありません。代わりtakeに、n番目の利回りの間に単に中断します。ブロック内で使用breakすると、ブロックを定義するフレームに制御が戻ります。a = [] ; InfiniteSeries.new.each { |x| a << x ; break if a.length == 10 } ; a
Matthew

22

入口と出口が定義されているクロージャーとは異なり、ファイバーはその状態を保持し、何度も戻る(降伏する)ことができます。

f = Fiber.new do
  puts 'some code'
  param = Fiber.yield 'return' # sent parameter, received parameter
  puts "received param: #{param}"
  Fiber.yield #nothing sent, nothing received 
  puts 'etc'
end

puts f.resume
f.resume 'param'
f.resume

これを印刷します:

some code
return
received param: param
etc

このロジックを他のルビ機能で実装すると、読みにくくなります。

この機能を使用すると、ファイバを適切に使用するために、(スレッドの置換として)手動で協調スケジューリングを行うことができます。Ilya Grigorikはeventmachine、非同期実行のIOスケジューリングの利点を失うことなく、非同期ライブラリ(この場合)を同期APIのように見えるようにする方法の良い例を持っています。こちらがリンクです。


ありがとうございました!私はドキュメントを読んだので、ファイバー内部の多くの入口と出口でこの魔法をすべて理解しています。しかし、これで生活が楽になるとは思いません。私はこのすべての履歴書と利回りを追跡しようとするのは良い考えだとは思いません。絡まりにくいクリューのようです。だから、この繊維のクリューが良い解決策である場合があるかどうかを理解したいと思います。Eventmachineはクールですが、ファイバーを理解するのに最適な場所ではありません。まず、このすべてのリアクターパターンについて理解する必要があります。そのためphysical meaning、より簡単な例でファイバーを理解できると思います
fl00r
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.