web-dev-qa-db-ja.com

繊維が必要な理由

ファイバーの場合、フィボナッチ数の生成という古典的な例があります。

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機能は使えませんか?

94
fl00r

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

おそらく、Rubyでの繊維の#1使用は、コアであるEnumeratorsを実装することですRuby class in Ruby 1.9。これらは信じられないほど便利です

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)>

これらのEnumeratorsはEnumerableオブジェクトであり、それらのeachメソッドは、ブロックで呼び出された場合、元の反復子メソッドによって生成された要素を生成します。先ほどの例では、reverse_eachによって返されるEnumeratorには、3,2,1を生成するeachメソッドがあります。 charsによって返される列挙子は、 "c"、 "b"、 "a"(など)を生成します。ただし、元のイテレータメソッドとは異なり、Enumeratorは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

ちょっと待ってください...そこに何か奇妙なものがありますか? an_iteratoryieldステートメントを直線コードとして記述しましたが、列挙子は一度に1つを実行できますnextの呼び出しの間、an_iteratorの実行は「凍結」されます。 nextを呼び出すたびに、次のyieldステートメントまで実行を続け、その後再び「フリーズ」します。

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

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

これはファイバーの性質に関係するものではありませんが、Enumeratorでできることをもう1つ述べます。それにより、each以外のイテレーターに高次のEnumerableメソッドを適用できます。考えてみてください。通常、mapselectinclude?injectなどを含むすべてのEnumerableメソッドall 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]

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

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

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プログラミングのように聞こえますか?)

その状態を明示的に保存し、クライアントが接続するたびにチェックするのではなく(次の「ステップ」が何であるかを確認するために)、各クライアントのファイバーを維持できます。クライアントを特定したら、そのファイバーを取得して再起動します。次に、各接続の終わりに、ファイバーを中断し、再度保管します。この方法で、すべてのステップを含む完全な対話のすべてのロジックを実装するための直線的なコードを書くことができます(プログラムがローカルで実行されるようにした場合と同じように)。

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

220
Alex D

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

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

他のRuby機能を使用したこのロジックの実装は読みにくくなります。

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

21