最近更新サボリすぎな寺岡です。
今回はRubyのProcに関するトリビアをご紹介します。
JavaScriptで変数隠蔽
JavaScriptではプロパティをprivateにして隠したりできないので、
どうしても隠蔽したい変数はクロージャのローカル変数に閉じ込めてしまうのが定石です。
1 2 3 4 5 6 7 8 9 10 11 12 |
function createCounter(start) { var count = start || 1; return function() { return count++; }; } var c1 = createCounter(); var c2 = createCounter(); console.log("c1: " + c1()); // c1: 1 console.log("c1: " + c1()); // c1: 2 console.log("c2: " + c2()); // c2: 1 console.log("c1: " + c1()); // c1: 3 |
Rubyで変数隠蔽
Rubyのprivateメソッドはsendで呼び出せるし、インスタンス変数はinstance_variable_getやinstance_evalで取り出せます。
そこで、JavaScriptの例と同じようにlambdaを使ってローカル変数に閉じ込めてみます。
1 2 3 4 5 6 7 8 9 10 11 |
def create_counter(count = 1) ->{ count.tap{ count += 1 } } end c1 = create_counter c2 = create_counter puts "c1: #{c1.()}" # c1: 1 puts "c1: #{c1.()}" # c1: 2 puts "c2: #{c2.()}" # c2: 1 puts "c1: #{c1.()}" # c1: 3 |
これでcount変数を外から直接参照したり出来ない筈。
ましてや書き換えるなんて不可能!!
……そう思っていた時期が私にもありました。
隠すどころか守れてすらいなかった orz
Proc#bindingのevalを使えば……
1 2 3 4 5 6 7 8 |
c3 = create_counter puts "c3: #{c3.()}" # c3: 1 c3.binding.eval('count = 100') puts "c3: #{c3.()}" # c3: 100 puts "c3: #{c3.()}" # c3: 101 |
なんということでしょう!
Procオブジェクトに隠されたローカル変数を取得、変更することが出来てしまいます。
もちろん、こんな邪悪な行いは許されるべきではありません。
ですがProc#bindingにも使い道はあるのです。
RubyのDSLにありがちなパターン
RubyのDSLではブロック内でグローバル関数のようにメソッドを呼び出すイディオムがよく使われます。
Railsのルーティングもその一例です。
1 2 3 4 |
MyApp::Application.routes.draw do resources :users resources :products end |
DSLを作ってみる
このイディオムを使った単純なDSLを提供するGreeterクラスを作ってみます。
ブロック内ではgreetメソッドを提供することにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Greeter def greet(name) puts "Hello #{name}!!" end def self.dsl(&block) new.instance_eval(&block) end end Greeter.dsl do greet "Jiro" # Hello Jiro!!" end |
ブロック内でgreetメソッドを呼び出すことが出来ました!
Rubyのブロック、Procは定義時のインスタンス(self)を保持しているため、
ブロックの中でもインスタン変数やインスタンスメソッドを呼び出すことが出来ます。
今回はブロック内のメソッド呼び出しをGreeterインスタンスに対する呼び出しに変更するため、
instance_evalによってブロックのselfを差し替えて実行しています。
外側のクラスのメソッドを使いたい
このDSLをRailsのコントローラ内で使ってみたくなったので、params[:name]を引数に渡してgreetメソッドを呼び出すことにしました。
1 2 3 4 5 6 7 |
class UsersController < ApplicationController def index Greeter.dsl do greet params[:name] # NameError: undefined local variable or method `params' for # end end end |
ブロック内のselfはGreeterインスタンスに差し替えられているため、UsersControllerのメソッドであるparamsを呼び出すことは出来ません。
ブロック内でGreeterとUsersController両方のメソッドを使うにはどうすればよいでしょうか?
Proc#bindingとmethod_missingの合わせ技
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Greeter def initialize(context) @context = context end def greet(name) puts "Hello #{name}!!" end def method_missing(name, *args, &block) @context.send(name, *args, &block) end def self.dsl(&block) new(block.binding.eval('self')).instance_eval(&block) end end |
コンストラクタで受け取った引数(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
1 2 3 4 5 6 7 |
class UsersController < ApplicationController def index Greeter.dsl do greet params[:name] # エラーにならずにログ(標準出力)に表示される end end end |
一見使い道の分からないメソッドにも、意外な活用法があったりするものですね。
Enjoy Ruby!!