Ruby: Procによる変数の隠蔽とbindng

最近更新サボリすぎな寺岡です。

今回はRubyのProcに関するトリビアをご紹介します。

JavaScriptで変数隠蔽

JavaScriptではプロパティをprivateにして隠したりできないので、
どうしても隠蔽したい変数はクロージャのローカル変数に閉じ込めてしまうのが定石です。

 

Rubyで変数隠蔽

Rubyのprivateメソッドはsendで呼び出せるし、インスタンス変数はinstance_variable_getやinstance_evalで取り出せます。
そこで、JavaScriptの例と同じようにlambdaを使ってローカル変数に閉じ込めてみます。

これでcount変数を外から直接参照したり出来ない筈。
ましてや書き換えるなんて不可能!!

……そう思っていた時期が私にもありました。

 

隠すどころか守れてすらいなかった orz

Proc#bindingのevalを使えば……

なんということでしょう!
Procオブジェクトに隠されたローカル変数を取得、変更することが出来てしまいます。

もちろん、こんな邪悪な行いは許されるべきではありません。
ですがProc#bindingにも使い道はあるのです。

 

RubyのDSLにありがちなパターン

RubyのDSLではブロック内でグローバル関数のようにメソッドを呼び出すイディオムがよく使われます。
Railsのルーティングもその一例です。

 

DSLを作ってみる

このイディオムを使った単純なDSLを提供するGreeterクラスを作ってみます。
ブロック内ではgreetメソッドを提供することにします。

ブロック内でgreetメソッドを呼び出すことが出来ました!

Rubyのブロック、Procは定義時のインスタンス(self)を保持しているため、
ブロックの中でもインスタン変数やインスタンスメソッドを呼び出すことが出来ます。

今回はブロック内のメソッド呼び出しをGreeterインスタンスに対する呼び出しに変更するため、
instance_evalによってブロックのselfを差し替えて実行しています。

 

外側のクラスのメソッドを使いたい

このDSLをRailsのコントローラ内で使ってみたくなったので、params[:name]を引数に渡してgreetメソッドを呼び出すことにしました。

ブロック内のselfはGreeterインスタンスに差し替えられているため、UsersControllerのメソッドであるparamsを呼び出すことは出来ません。

ブロック内でGreeterとUsersController両方のメソッドを使うにはどうすればよいでしょうか?

 

Proc#bindingとmethod_missingの合わせ技

コンストラクタで受け取った引数(context)に対して、method_missingでsendを呼ぶようになりました。
こうすると、Greeterクラスにメソッドが見つからない場合は@contextに対するメソッド呼び出しに変換することができます。

後はコンストラクタにcontextとしてUsersControllerのインスタンスを与えるだけです。

ここでようやくProc#bindingの出番がやって来ました。
block.binding.eval('self') を呼び出せば、「blockを定義した場所でのself」つまりUsersControllerのインスタンスを取得することが出来るのです!!

2013/9/19 追記:
Binding#eval('self')相当のメソッドが、ruby2.1で Binding#receiver という名前で採用されるかもしれません。 http://bugs.ruby-lang.org/issues/8779

一見使い道の分からないメソッドにも、意外な活用法があったりするものですね。

Enjoy Ruby!!

Comments are closed, but you can leave a trackback: Trackback URL.