1. ホーム
  2. ruby

[解決済み] なぜ、繊維が必要なのか

2022-11-16 14:32:35

疑問点

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

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の機能ではできない、ファイバーを使ったどんなことが書けるのでしょうか?

どうやって解決するの?

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

Rubyでファイバーを使う一番の方法は、おそらく Enumerator を実装することです。これは 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)>

これらは Enumerator はEnumerableオブジェクトであり、その each メソッドは、元のイテレータメソッドがブロック付きで呼び出された場合に 得られるはずだった要素を得ます。先程の例では reverse_each が返す Enumerator には each メソッドがあり、3,2,1 を返します。が返すEnumeratorは chars は "c","b","a" (といった具合に) を返します。しかし、本来のイテレータメソッドとは異なり、Enumeratorは、次のように呼び出すと、要素を一つずつ返すこともできます。 next を繰り返し呼び出すと、要素を一つずつ返すことができます。

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" Design Patterns」の本に記載されています)。上記の例では、Enumerator を使用して内部イテレータを外部イテレータに変えることができることを示しています。

これは、独自の列挙子を作るための一つの方法です。

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 を直線的なコードとして実行することができますが、Enumeratorはそれらを を一度に一つずつ . の呼び出しの間に next の実行は an_iterator は "frozen"である。呼び出すたびに next を呼び出すたびに、以下のように実行され続けます。 yield ステートメントまで実行し続け、その後再び "フリーズ" します。

これがどのように実装されているかわかりますか?Enumerator は以下の呼び出しをラップしています。 an_iterator の呼び出しをファイバーで包み、そのブロックを ファイバーを一時停止する . そのため、毎回 an_iterator がブロックに降伏するたびに、それが実行されているファイバーは中断され、メインスレッドで実行が継続されます。次に next を呼び出すと、ファイバーに制御が渡されます。 を呼び出すと、ブロックは そして an_iterator はそれが去ったところから続けます。

ファイバーなしでこれを行うために何が必要かを考えるのは有益でしょう。内部と外部の両方のイテレータを提供したい EVERY クラスは、次の呼び出しの間に状態を追跡する明示的なコードを含む必要があります。 next . next を呼び出すたびにその状態をチェックし、値を返す前にそれを更新しなければなりません。ファイバーを使用すると 自動的に 内部イテレータを外部イテレータに変換することができます。

これはファイバーとは関係ありませんが、もうひとつEnumeratorでできることを紹介します。それは、高階のEnumerableメソッドを each . 考えてみてください。通常、すべてのEnumerableメソッド、たとえば map , select , include? , 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]

ブロックなしでイテレータを呼び出すとEnumeratorが返され、それに対して他の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]

これがボンネットの中でファイバーを使用しているかどうかは分かりませんが、使用することは可能です。ファイバーは無限リストや系列の遅延評価を実装するために使うことができます。Enumerator で定義された遅延メソッドの例として、私はここでいくつかのメソッドを定義しています。 https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb

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

これで少しは可能性が見えてきたのではないでしょうか。冒頭で述べたように、ファイバーは低レベルのフロー制御のプリミティブです。これにより、プログラム内で複数の制御フローの位置 (本のページにおける異なるしおりのようなもの) を維持し、必要に応じて切り替えることができます。任意のコードをファイバーで実行できるので、ファイバー上のサードパーティのコードを呼び出して、それが自分の制御するコードにコールバックされたときに、フリーズして別の処理を継続することができます。

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

その状態を明示的に保存し、クライアントが接続するたびに (次に何をしなければならないかを確認するために) それをチェックするのではなく、各クライアントのファイバを維持することができます。クライアントを特定したら、そのファイバーを取り出して再スタートさせます。そして、接続が終わるたびにファイバーを一時停止して、再び保存します。この方法では、すべてのステップを含む完全な対話のためのすべてのロジックを実装するために、まっすぐなコードを書くことができます (プログラムがローカルで実行されるように作られている場合は、当然そうなります)。

このようなことが実用的でない理由はたくさんあると思いますが (少なくとも今のところは)、もう一度言いますが、私はただ可能性のいくつかをお見せしたいだけなのです。一度コンセプトを理解すれば、他の誰も思いつかないようなまったく新しいアプリケーションを思いつくかもしれません!