Opalはどうやってmethod_missingを実装しているのか
本記事はOpal Advent Calendar 2016の17日目の記事です。
OpalはRubyからJavaScriptへのコンパイラです。今回はRubyの黒魔術の一つであるmethod_missingの実装について見ていきます。
method_missingとは
Module#method_missingは、あるオブジェクトに対して定義されていないメソッドを呼び出したときに走るフックを定義する機能です。
class A
def method_missing(name, *args)
puts "hooked (name: #{name}, args: #{args.inspect})"
end
end
A.new.foo(1, 2, 3)
上記を実行すると以下のようなメッセージが出力されます。
hooked (name: foo, args: [1, 2, 3])
クラスAにはfooというメソッドがないので、fooの代わりにmethod_missingメソッドが呼ばれるということですね。用途としては、DSLを作る際に使われたりします。
どうやって実装するか?
Opalはこの機能をどうやって実装しているのでしょうか。Opalでは、RubyのメソッドはJavaScriptのメソッド呼び出しに変換されます。
a.foo()
a.$foo();
残念ながら、JavaScriptには存在しないメソッド呼び出しをフックする機能はなく、呼び出そうとすると例外が発生します。
これを回避する単純な方法としては、「メソッド呼び出しの前に毎回、メソッドが存在するか確認する」というやり方があります。実際、昔のOpalはこのようにしていたのですが、これだとmethod_missingを使っていない場合でも動作速度が落ちるという問題があります。
メソッド呼び出しを列挙する
今のOpalはもっと良い方法を使っています。まず、実行するソースコードを解析して、メソッド呼び出しをすべて列挙します。(解析というと大変そうですが、どのみちコンパイルするためにはパースしないといけないので、そこからメソッド呼び出しを列挙するのは簡単です。)
その後、継承関係の上の方(BasicObject)にこれらの名前のメソッドを実装し、method_missingメソッドを呼ぶように設定しておきます。コンパイル後のソースコードのadd_stubsを呼んでいる箇所がそれです。
Opal.add_stubs(["$foo"]);
これにより、fooメソッドをもたないオブジェクトに対してfooを呼ぼうとするとBasicObject#fooが呼ばれ、めでたくmethod_missingが呼ばれるようになります。
補足:sendを使った場合は?
Rubyに詳しい人なら、上の方法では「メソッド呼び出しの一覧」を網羅できないことに気づいたかもしれません。Kernel#sendを使うと、以下のように任意の式がメソッド名になる可能性があるので、実行するまでメソッド名が確定しません。
a.send(["foo", "bar"].sample) #=> fooかbarのどちらかを呼び出す
しかしsendの場合は実行時にメソッド名が取れるわけなので、sendの側でメソッドがなければmethod_missingを呼ぶよう実装されています。抜かりありませんね。
まとめ
- 全部のメソッド呼び出しにフックを入れるのは遅い
- method_missingかもしれないメソッド名を全部列挙する
- sendはsend側で対処