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では「メソッドはクラスに属し、インスタンスはクラスへのリンクを持っているだけ」というのが理解できる。