irb の Tips - サブ irb を使う

この記事は Ruby Advent Calendar jp: 2009 : ATND が楽しそうなので触発されて書いてますが参加してはいません。飛び入りですみません。あとまさかのネタ被りしてたら本当にごめんなさい。

sub irb

Rubyist の必需品 irb 多くの人が愛用していることと思います。わたしも大好きです。メソッドの挙動を確認したり、ちょっとした作業の補佐に使ったり、あるいは電卓がわりに、常に起動しているプロセスのひとつです。

けど irb に サブirb を起動するという機能があることを知らない、あるいは知ってるけど使いどころがわからないでのあまり使ってないという人は多いのではないでしょうか。

サブirbRubyリファレンスマニュアルにも書かれていて、拡張ではない(require は必要ない)基本機能です。「使用例」の項に書かれているように、irb メソッドによって新しい irb インスタンスを起動することができます。サブirb はそれぞれ固有の Thread で動作します。また irb メソッドに引数としてオブジェクトを渡すことでコンテキスト(self)を置き換えた状態でサブirb を起動することもできます。*1

% irb
irb(main):001:0> class A
irb(main):002:1> end
=> nil
irb(main):003:0> irb A       # クラス A を working space としてサブirbを起動
irb#1(A):001:0> self
=> A                         # self が A になっている
irb#1(A):002:0> def meth1    # ここでメソッドを定義すると……  
irb#1(A):003:1> p "A#meth1"
irb#1(A):004:1> end
=> nil
irb#1(A):005:0> jobs         # jobs (または irb_jobs) で irb 一覧を表示
=> #0->irb on main (#<Thread:0x00000100886318>: stop)
#1->irb#1 on A (#<Thread:0x0000010085c7e8>: running)
irb#1(A):006:0> fg 0         # fg (または irb_fg) でスイッチ
=> #<IRB::Irb: @context=#<IRB::Context:0x000001010bb830>, @signal_status=:IN_EVAL, @scanner=#<RubyLex:0x000001010a7448>>
irb(main):004:0> A.instance_methods false
=> [:meth1]                  # A のインスタンスメソッドが定義されています

ブロック内で sub irb を起動

さて、このサブirbという機能、irb メソッドで新しくサブirb を起動すると元の Thread はその時点で停止し、irb_fg で切り替えられない限り処理が先に進みません。このことを利用すると、ブロックをとるメソッドのブロック内の処理を irb のプロンプトからインタラクティブに実行できます。これは例えば Kernel#open のように、ブロックを使うことが推奨されるメソッド(ブロックを抜けると自動的に File が close されるので)を irb で利用するときに「本当はブロックを使うほうがいいんだけどな」と思いつつ file = open(...) などとしなくて済みます。またブロックつきの呼び出ししかサポートしていないような厳格なメソッドをデモしようとする場合に便利です。まあ、拙作の Ruby/PureDataPureData.start メソッドがそういう仕様になっているので、この方法に辿りついたのですけれど。

では Ruby/PureData のデモを irb でやってみます*2

irb(main):001:0> Pd.start do |pd| irb(pd) end  # Pd.start のブロック内でサブirb を起動
:    # (Pd 起動メッセージ略)
:    # ここから Pd.start のブロック内の処理をインタラクティブに実行できます
irb#1(#<PureData:>):001:0> cv = canvas("sample.pd")      # Pd::Canvas を作成
irb#1(#<PureData:>):002:0> osc = cv.obj("osc~", 440)     # オシレータ作成
irb#1(#<PureData:>):003:0> dac = cv.obj("dac~")          # DAC(オーディオ出力)作成
irb#1(#<PureData:>):004:0> dac.left << osc >> dac.right  # 結線してます
irb#1(#<PureData:>):005:0> self.dsp = true               # Audio処理開始
                                          # 実際はここで ポー って音が鳴ってます
irb#1(#<PureData:>):006:0> self.dsp = false              # Audio処理停止
irb#1(#<PureData:>):007:0> irb_exit                      # irb_exit することで
EOF on socket 10     # irb_exit することで Pd.start のブロックを抜けます
                     # Pd.start の場合ブロックを抜ける時に Pd のプロセスを停止します

ただしこの用途の場合は irb_fg でブロック付きのメソッドを実行している元のサブirb に切り替えると処理が続行され、その時点でブロックを抜けてしまいます。Kernel#open やこの PureData.start の場合はブロックを抜ける時に後始末の処理(File の close や子プロセスの停止)が実行されるので、ブロック内の処理が続行できなくなってしまうため irb_fg で切り替えする時には気をつけなくてはいけません。

さあ、これで「open みたいなメソッドをブロック付きで呼び出すのは irb の時に不便だから」という不安はありません*3。心置きなく推奨されるブロック付きのメソッド呼び出しを駆使しましょう!

*1:なお同じ irb インスタンスでコンテキストだけ切り替える chws/pushws/popws などのメソッドも存在します。

*2:Pd の起動時のメッセージや inspect 結果は長いので削ってます

*3:実際にはローカル変数のスコープも切り替わってしまうので不便な場合もありますが……