ホーム DoRuby Ruby黒魔術【callcc】でFiberを実装してみた
 

Ruby黒魔術【callcc】でFiberを実装してみた

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

以前rubyの大域脱出を紹介した際にrubyには継続渡しがあると話をしたので、簡単に解説してみた上で継続渡しとFiberの理解のためcallccを使ったEvilFiberを実装してみました。

はじめに

17新卒エンジニアくろすです。
使ってたBluetoothイヤホンが断線したり、代わりに通販で買ったイヤホンが初期不良で電源入らなかったりと本厄の洗礼が激しく、お祓いに行こうか真剣に悩んでます。
誰かオススメの厄払いがある方いらっしゃいましたら連絡をください。

今回は楽しい黒魔術の話だけ書いてパパッと終わらせちゃうぞ、と思っていました。
思っていました。
callccからFiberの話なんか始めちゃったのが間違いでした。

環境等

環境 : ruby-2.4.2
callccを使おうとcontinuationをrequireすると

warning: callcc is obsolete; use Fiber instead

と怒られます。

処理の中断、再開として使うなら Fiber
大域脱出として使うなら throw..catch
があるのでそちらを使いましょう。

黄泉還りを楽しみたい方は遊びの範囲内で使いましょう。

黒魔術

メタプログラミングによる動的なコード生成や加工が黒魔術と呼ばれることが多いです。
Rubyで有名なのは
与えられた文字列をrubyのコードとして解釈するようなeval属
動的なメソッドを生成、またはそのメソッドが存在するかのように見せかけるdefine_methodmethod_missingによるゴーストメソッド
などがあります。
今回はRuby黒魔術の中でも黄泉還りが危険すぎてrequireしないと使えないようになってしまっているcallccについて軽く触れていきたいと思います。

Rubyの継続

require 'continuation'

x = 1
if x == 1
  y = x * callcc{|cont| @a = cont; x}
  puts y
end
if y < 5
  x += 1
  @a.call(x)
end

callccを呼ぶことでcallcc以降のコンテキスト(継続)@aに保存して、@a.call(x)callcc{|cont| @a = cont; x}の返り値に、call時のxの値を代入して5行目に戻ります。
パッと見、yにはx2 が入りそうな感じがしますが、Ruby的には最初に5行目を評価した時点で、xの値も評価し終わっているようで結果は以下のようになります。

$ ruby black_magic.rb
1
2
3
4
5

一歩間違えば無限ループ入りまっしぐらなのが見てわかるかと。

ちなみに、5行目を以下のように変更すると

y = callcc{|cont| @a = cont; x} * x

結果はこうなります。

$ ruby black_magic.rb
1
4
9

ざっくり継続の感じが掴めましたでしょうか。

Fiber

話が少し飛びますが、そもそもFiberとはなんぞ?という疑問がありそうなので軽く引用します。

ノンプリエンプティブな軽量スレッド(以下ファイバーと呼ぶ)を提供します。 他の言語では coroutine あるいは semicoroutine と呼ばれることもあります。 Thread と違いユーザレベルスレッドとして実装されています。

引用 : Ruby2.5.0 Fiberクラスリファレンス

Rubyはそもそもの言語仕様として同時に実行されるネイティブスレッドは常にひとつだったりするんで、あくまでRubyVM内でノンプリエンプティブだって話ですが……
参考 : Ruby2.5.0 Threadクラスリファレンス

もう完全にコルーチンなんですが、要するにrubyのコードレベルで明示的にコンテキストを変更できる軽量なスレッド(スレッドではない)です。
Ruby的にわかりやすく言うならば処理の中断再開が可能なProcオブジェクト的な感じです。
以下のようにfiber自体は同一スレッド内でコンテキストを変更して動きますし、別スレッドからresumeしようとするとFiberErrorって怒られます。

require 'fiber'

fiber =
  Fiber.new do
    loop do
      Fiber.yield Thread.current.object_id
    end
  end

puts <<-EOS
===========================
  parent : #{Thread.current.object_id}
  child  : #{fiber.resume}
===========================
EOS
#=>
# ===========================
#   parent : 70103219828660
#   child  : 70103219828660
# ===========================

EvilFiber

callccを使い実装した邪悪なFiberです。
このブログのcounterを参考にさらに抽象度を上げてFiberに近づけた形になります。
元記事でも言われてますが、stopとresumeのスパゲッティ具合がとにかく邪悪です。
限界までFiberとfiber(コルーチンライクに使うための標準ライブラリ)に合わせようと思いましたが、#transferとスレッドセーフを実装する気力はなかったです……

require 'continuation'

class EvilFiber
  @@fibers_hash = {}
  @@current = nil
  @@prev = nil

  class << self
    def yield(*args)
      @@current.stop(*args)
    end

    def current
      @@current
    end

    def prev
      @@prev
    end

    private
    def fibers_hash
      @@fibers_hash
    end
  end

  def initialize
    @@fibers_hash[self.object_id] = self
    @resume =
      proc do
        yield
        @dead = true
        reset_registered_fiber
        nil
      end
  end

  def stop(*args)
    reset_registered_fiber
    callcc do |cont|
      @resume = cont
      @return.call(*args)
    end
  end

  def resume
    raise 'THIS FIBER DIED!!!' if self.dead?
    @@current = @@fibers_hash[self.object_id]
    callcc do |ret|
      @return = ret
      @resume.call
    end
  end

  def alive?
    not @dead
  end

  def dead?
    !!@dead
  end

  private
  def reset_registered_fiber
    @@prev, @@current = @@current, nil
  end

end
[1] pry(main)> require_relative 'evil_fiber'
warning: callcc is obsolete; use Fiber instead
=> true
[2] pry(main)> f = EvilFiber.new{ puts '小烏丸'; EvilFiber.yield; puts '蜥蜴丸'; EvilFiber.yield; puts '小狐丸' }
=> #<EvilFiber:0x00007fc61a0e7dc8 @resume=#<Proc:0x00007fc61a0e7d50>>
[3] pry(main)> f.resume
小烏丸
[4] pry(main)> f.resume
蜥蜴丸
[5] pry(main)> f.resume
小狐丸
[6] pry(main)> f.resume
RuntimeError: THIS FIBER DIED!!!

new時に渡しているブロック内のEvilFiber.yieldf.yieldにするという実装もあったのですが、コードとして見たときにパッと見未生成に見えるオブジェクトを触っているのが気持ち悪いので、Fiber側に合わせる方向にしました。
実際渡してるブロックが評価されるのはブロックがcallされた時(つまりこのコードだと最初にresumeが呼ばれた時)で、mainのオブジェクトのBindingから変数fを引っ張ってくるはずなので動くと思うんですが気持ち悪いものは気持ち悪いです。
オブジェクトの生成とブロックの登録を分ければ多少気持ち悪さが解消される気がします。

終わりに

ざっくりとですが、callccとFiberの勉強ができていい機会になったなって思います。
特にFiberですが、Threadよりコードに寄ってくれるので個人的にはかなり楽に書けるなと印象でした。
マルチスレッドプログラミングじゃなくてただのコルーチンと化した使い方しかしていないのでそりゃそうですけど。
一度ちゃんとマルチスレッドプログラミング勉強しないとなぁ……

記事を共有
モバイルバージョンを終了