メタプログラミングRubyの4章を読んでアウトプットがてら解説記事を書いてみようとおもいます。
まずは基本から
まずはブロックとは何かという基本的なものを確認しましょう。
do ~ end
や{}
で定義されたもので、メソッドが呼び出されたときのみブロックを定義できます。
そして、ブロックはどこでも存在できるコードではないんです。ローカル変数やselfといったものが必要になるんです。
これらはオブジェクトと関連性をもったもので束縛と呼ばれることもあります。
実際にコードを見ていきましょう
def ex_method
yield
end
var = 1
ex_method do
var += 1
local_var = 0
end
var #=> 2
local_var #=> NameError (undefined local variable or method `local_var' for main:Object)
上記のコードのように、ブロック内で新しく束縛を定義したとき、ブロックが終了した時点でその束縛は消えてしまう。
だからこそNameErrorが発生している。
これらを深く学んでいくためには、スコープを知っていく必要があります。
スコープ
どんな風にスコープが変化するかを実際に見ていきましょう
var1 = 1
class Example
var2 = 2
local_variables # => [:var2]
def ex_method
var3 = 3
local_variables
end
local_variables # => [:var2]
end
obj = Example.new
obj.ex_method #=> [:var3]
obj.ex_method #=> [:var3]
local_variables
新しいスコープに入ると、それまでの束縛は新しい束縛に取って代わられるんです。
今回の例でいえば、Exampleクラスに入るとvar1
はスコープ外となるんです。
そしてExampleクラスのクラス定義が終わると、トップレベルのスコープ、つまりvar1
に戻るんです。
Exampleクラスのオブジェクトを作成して、ex_methodを参照しているとき、どんなことが起きているんでしょうか?
1回目にex_methodを参照するときは、新たにスコープをオープンしてローカル変数であるvar3
が呼び出されている。
そしてメソッドを終了させてExampleクラスのスコープに戻り、メソッドのスコープは終了します。
2回目にex_methodを参照すると、別の新たなスコープをオープンして、新たなローカル変数であるvar3
が呼び出されています。
見た目は同じように見えますが、object_id
を調べてみると違うことが分かるかとおもいます。
このように、スコープを変えると、それまでの束縛と新たな束縛が置き換わるんです。これが重要なポイントです。
フラットスコープ
スコープが変わるとそれまでの束縛と新たな束縛が置き換わることが分かったとおもいます。
では、以下のような場合どうしたらよいでしょうか?
var1 = "OK"
class Example
# ここにvar1 = "OK"を表示したい
def ex_method
# ここにvar1 = "OK"を表示したい
end
end
classをスコープゲートではない何かに置き換えればいいんです。メソッド呼び出しをするといいんです。
class.new
にブロックを渡すとクラスにインスタンスメソッドを定義できるんです。
var1 = "OK"
Example = Class.new do
# ここにvar1 = "OK"を表示したい
puts "表示:#{var1}" #=>表示:OK
def ex_method
# ここにvar1 = "OK"を表示したい
end
end
つぎにdef内にvar1
を表示させるにはどうしたらいいでしょうか?define_method
を使ういいです。
var1 = "OK"
Example = Class.new do
# ここにvar1 = "OK"を表示したい
puts "表示:#{var1}" #=>表示:OK
define_method :ex_method do
puts "ここに表示されるのは#{var1}です"
end
end
puts Example.new.ex_method
# =>ここに表示されるのはOKです
このようにすることで、他のスコープの変数を参照できるようになるんです。
これをフラットスコープといいます。スコープを参照できるできないの差を無くす、つまりフラットにするという意味ですね。
define_method
,Class.new
,Module.new
を使うことで、あるスコープは束縛を参照できる、できないを自由自在に扱えるようになるんですね。すごい(笑)
instance_eval
もう一個コードと束縛を自由自在に操られるメソッドがあります。それがinstance_eval
というものです。
実際にコードを見ていきましょう。
class Example
def initialize
@v = 'hoge'
end
end
obj = Example.new
obj.instance_eval do
self => #<Example:0x000001c897df4678 @v="hoge">
@v => #"hoge"
end
instance_evalに渡したブロックは、レシーバがselfになってから評価されるので、レシーバのあらゆる値(privateメソッドやインスタンス変数など)を参照できるんです。
このようにinstance_evalに渡したブロックのことをコンテキスト探査機と呼ばれます。個人的にはレシーバのあらゆる値が参照できてしまうのは、いいような悪いような気がします。
Procとlambdaについて
procについてこちらの記事に以前記事にしたのでこちらを参考にしてください。めちゃ簡単にいうとブロックをオブジェクト化したものです。
Procとlambdaの違いはめちゃめちゃアイマイなんです。この2つの違いを深堀は今回はしませんが、押さえておきたい2つの違いがあります。
return
1つ目の違いは、returnキーワードに関してです。
lambdaはreturnした後にメソッドに戻り、メソッドを最後まで実行するのに対して、Procはreturn後にメソッド自体を抜けてしまいます。
言葉で言われてもわからないので実際にコードを見てみましょう。
# lambdaの場合
def lambda_method
lambda1 = lambda { return p "lambda"}
lambda1.call
p "lambda method"
end
lambda_method #=> "lambda"
#=> "lambda method"
# Procの場合
def proc_method
proc = Proc.new { return p "proc"}
proc.call
p "proc method"
end
proc_method #=> "proc"
lambdaの場合は、単にlambdaから戻っているため、メソッド内の処理をすべて実行してくれます。
それにたいして、Procのときは、returnした後メソッドから抜けてしまうため、proc method
という文言が出力されないんです。
引数の違い
2つ目の違いは引数のチェック方法が違います。実際にコードを見ていきましょう。
# Procの場合
proc1 = proc { |arg| p arg }
proc1.call( 'proc', 'lambda') #=> "proc"
# lambdaの場合
lamd = lambda { |arg| p arg }
lamd.call('lambda') #=> "lambda"
lamd.call('lambda', 'proc') #=> wrong number of arguments (given 2, expected 1) (ArgumentError)
Proc
の場合は、指定している引数より多い場合、多い分は無視してくれるんですね。
それにたいして、lambda
の場合は、指定している引数より多い場合、ArgumentError
が出るんです。
個人的には、lambdaをつかうのがいいのかなとおもいます。
DSLについてまとめようと思ったのですが、ちゃんと理解できなかったのでまたの機会にしようかとおもいます。
以上です。何か間違いがございましたら、ご教示いただけますと幸いです。
【参考資料】