@m_seki の

I like ruby tooから引っ越し

長生きirb

長生きするirbを書きたくなったのでその素振り。複数のirbインターフェイスが実は一つのインタプリタを共有している、ということをやってみる。

まず、クライアント(?)。

require 'drb/drb'
require 'irb/input-method'

STDOUT.sync = true

DRb.start_service
im = IRB::StdioInputMethod.new
im.extend(DRbUndumped)

ro = DRbObject.new_with_uri('druby://localhost:54345')
th = ro.start(im, $stdout)
th.join

input-methodと標準出力をサーバ(???)に渡して、irbをstartします。クライアントはわりときれい。
私の環境のOSXのreadlineはスレッドが固まってしまうので、StdioInputMethodを使いました。
出力側はちょっとよくわからなくて、output-methodではなく$stdoutを与えました。
output-methodっていうのはどういうときに使うんだろう。プロンプトの印字ははoutput-methodを経由してくれないのですよね。

つぎにサーバ側。一部は既存のメソッドを置き換えます。前述したように出力に関してはかなり苦労しています。IRB::Contextでのprintとprintfをクライアント側に回すために、@stdoutを新設しました。もし指定されてたらそっちへ委譲しちゃうのだ。

require 'irb'
require 'drb/drb'

module IRB
  class Context
  • 追記。verbose?じゃなくてprompting?だったみたい。
    def verbose?
      true
    end
  end

  class Irb
    attr_accessor :stdout
    def print(*opts)
      @stdout ? @stdout.print(*opts) : super
    end

    def printf(*opts)
      @stdout ? @stdout.printf(*opts) : super
    end
  end

  def IRB.conf; @CONF; end
end

class IRb
  def start(input_method, output)
    irb = IRB::Irb.new(nil, input_method)
    irb.stdout = output
    IRB.conf[:MAIN_CONTEXT] = irb.context
    th = Thread.new do
      catch(:IRB_EXIT) do
        irb.eval_input
      end
    end
  end
end

IRB.setup(__FILE__)
DRb.start_service('druby://:54345', IRb.new)
DRb.thread.join

もしも、readlineでブロックしない環境なら、最後の部分を次のようにしたらよいと思うよ。

DRb.start_service('druby://:54345', IRb.new)
IRB.start(__FILE__)

そうすると、サーバ自身もirbインターフェイスとして動くので1.5倍くらいたのしい。

使い方は、端末をたくさん用意して、端末1でサーバを実行し、端末2からnでクライアントを実行するだけ。

それぞれの端末にbindingができるのでローカル変数等は混じらないけど、グローバル変数を経由してオブジェクトを交換できます。グローバル変数にRinda::TupleSpaceを置いといてそれぞれの端末でオブジェクトを投げ合うと楽しいかもね。

[term-1] サーバ側の起動
% ruby irb_d.rb

[term-2] クライアント1の起動
% ruby irb_c.rb
irb(main):001:0> a = []
a = []
=> []

[term-3] クライアント2の起動。[term-2]のaは[term-3]からは見えないよ。
% ruby irb_c.rb
irb(main):001:0> a
a
NameError: undefined local variable or method `a' for main:Object
	from (irb):1
	from :0

[term-3] クラスFooを定義する。大文字で始まるものはグローバルなので、きっとみんな見える。
irb(main):002:0> class Foo
class Foo
irb(main):003:1> attr :foo
attr_accessor :foo
irb(main):004:1> end
end
=> nil

[term-2] Fooを使ってみる。
irb(main):002:0> foo = Foo.new
foo = Foo.new
=> #<Foo:0x45c360>
irb(main):003:0> foo.foo = 'foo'
foo.foo = 'foo'
=> "foo"

[term-2] TsにTupleSpaceをいれて、[:hello, nil]でブロックしてみよう
irb(main):005:0> require 'rinda/tuplespace'
require 'rinda/tuplespace'
=> true
irb(main):006:0> Ts = Rinda::TupleSpace.new(5)
Ts = Rinda::TupleSpace.new(5)
=> #<Rinda::TupleSpace:0x42967c ...>
irb(main):007:0> Ts.take([:hello, nil])

[term-3] Tsは見えるので、[:hello, :again]タプルを投入してみる
irb(main):007:0> Ts.write([:hello, :again])
Ts.write([:hello, :again])
=> #<Rinda::TupleEntry:0...>

[term-2] ブロックがとけ、タプルが届いたよ!!
Ts.take([:hello, nil])
=> [:hello, :again]

とかさあ。

inf-rubyから使うにはどうしたらいいかな。