Proc & Lambda & Method
ブロックは原則宣言したその場で評価が行われるが、この評価を後から実施したい場合があり、これが出来るのがProcやlambdaの利点となっている。
また、ブロックはyeild
で、ブロックの渡し先で実行を行うことが出来るが、渡された先で更に別のメソッドにブロックを渡したい場合がある。この場合、yeild
だけでは、どのブロックを渡しているかわからなくなる。渡されたブロックに、ちゃんとした名前を付けてやるのが&修飾
である。
def math(a,b) p yield(a,b) end def do_math(a,b,&operation) math(a,b,&operation) #無名のまま渡すとここで、渡すブロックを指定できない。 end do_math(2,3){|x , y| x*y}
なお、仮引数でブロックを受け取った部分に&
を付けたため、このブロックは一度Proc
として評価されている。
def do_math(a,b,&operation) p operation.class # Proc p operation.call(a,b) # 6 end do_math(2,3){|x , y| x*y}
逆にProcからブロックに戻したい場合も、&
を使えばよい。
def math(a,b) p block_given? # true p yield(a,b) # 6 end def do_math(a,b,&operation) math(a,b,&operation) end do_math(2,3){|x , y| x*y}
ちなみにProcからBlockへの変換は、メソッド内部では行えない。
to_block = &operation # proc.rb:7: syntax error, unexpected & # to_block = &operation
必ず、変換は実引数の中で行う必要がある。こういうことは出来ない。
def math(a,b,&operation) #仮引数の中でBlockに変換させる。 p block_given? # true p yield(a,b) # 6 end def do_math(a,b,&operation) # Procに変換したまま渡す。 math(a,b,operation) end do_math(2,3){|x , y| x*y}
proc.rb:1:in `math': wrong number of arguments (given 3, expected 2) (ArgumentError)
渡した先でブロックとして用いたいなら、渡す前にブロックに変換しておく必要がある。 「ブロックは常に渡された時点で既に無名でなければならない。(渡してから無名化は出来ない。)」と覚えておけばよい。
lambdaとblockの違いに関しては既にsilverの勉強で扱っている。
定義した処理内にreturnを定義したとき、ラムダ式は「定義された処理だけ終了し、次の処理に移行する」のに対して、Procは「Procがcallされたスコープから脱出する」という不思議な挙動をする
Ruby Silver 試験対策 Day3 (ブロックとProcと例外編) - Pyons Tech Blog
この点ではlambdaの方が遥かに素直な挙動をすると感じられる。
また引数に関してもlambdaの方が、「足らなければエラー」というメソッドに近い挙動を見せる。
例えば、今まで見てきた「Procで定義された処理に対して引数が足らない場合の処理」が異なる。Argument Errorを出さず、nilをセットしていたProcに対しラムダ式はArgumentErrorを出す。
Ruby Silver 試験対策 Day3 (ブロックとProcと例外編) - Pyons Tech Blog
この2つの違いを見るかぎり、lambdaの方が遥かにメソッドの動きに近く、使いやすそうだとわかる。本の中でもlambdaの方が好まれている旨が書かれている。
最後にMethodを見ていく。
Object#methodというメソッドを用いると、メソッドそのものをlamdaなどのように取得することが出来る。取得したメソッドはProc
と同じくcall
で呼び出すことが出来る。ただし、その評価が行われるスコープは「元いたオブジェクトの中」である。したがって、メソッドの取得元のオブジェクトのインスタンス変数なども処理に反映される。
class MyClass def initialize @variable = 100 end def a_method(param) @variable * param end end object = MyClass.new fetched_method = object.method :a_method p fetched_method.class # Method p fetched_method.call(7) # 700
この例では、取得したオブジェクトはあくまで、取得元のオブジェクトというスコープの中で動いていた。
ここからさらにに、メソッドをオブジェクトクラスから切り離して別のオブジェクトクラスに与える、という芸当が出来てしまう。
class MyClass def initialize @variable = 100 end def original_method(param) @variable * param end end fetched_method = MyClass.instance_method(:original_method) p fetched_method.class # UnboundMethod class MyChildClass < MyClass def initialize @variable = 200 end end obj2 = MyChildClass.new MyChildClass.send :define_method, :copied_method , fetched_method p obj2.copied_method(3) # 600
課題を勘違いして、オブジェクトに対して新しいメソッドを使いしようとしたところ、それは出来なかった。
obj2オブジェクト生成後に親クラスのメソッドを子クラスにコピーしているにも関わらず、ちゃんと期待した通りメソッドが実行できており、Rubyでは「メソッドはクラスに属し、インスタンスはクラスへのリンクを持っているだけ」というのが理解できる。