Rubyをはじめると、ブロックとかProcとかLambdaとか、
みんな似たような雰囲気をしているから、うまく理解しづらいですよね。
復習も兼ねて、余計な説明抜きで、ブロック・Proc・Lambdaについて説明していきます。
ブロック
ブロックはクロージャです。 ですので、ブロックを使うことで、普段参照できないローカル変数を参照できるようになります。class Sample
x = 1
y = 1
define_method :output do
x + y
end
end
sample = Sample.new
puts sample.output # => 2
上記のコードにある、define_methodの、
doからendまでの部分({}を利用することもできます)がブロックです。
each文やmapもブロックのロジックを実行します。
x = 10
array = [1, 2, 3, 4]
# each
array.each do |num|
puts x + num # => 11, 12, 13, 14
end
# map
array.map do |num|
puts num * x # => 10, 20, 30, 40
end
また、ブロック内のロジックが一文だけなら、
# each
array.each { |num| puts x + num }
# map
array.map { |num| puts num * x }
このように記述すると読みやすいかもしれません。
ブロックはインスタンスメソッドに渡して、メソッド内で利用することもできます。
x = 1
def output
yield * 10
end
puts output { x * 10 } # => 100
outputメソッドはスコープの違う変数xを参照することができませんが、
クロージャであるブロックは別です。
outputの引数としてxを含めたブロックを渡し、メソッド内のyieldでそれを展開しています。
ブロックは、メソッド定義時に明示しなくても、呼び出し時に引数としてブロックを記述することで、ブロックが渡されたと判断されます。
その後にyieldがブロックを展開するのです。
こうして、メソッドはそのスコープの外にあるxを利用して、計算を実行することができるようになりました。
上のコードの場合、outputメソッドの中身は、
(x * 10) * 10
となるわけです。
Proc
しかし、ブロックは利用の仕方に限りがあります。ブロックは、いわば、バラバラのコードを束ねた塊に過ぎず、これ自体はオブジェクトとして扱われません。
ですので、ブロックは変数に渡すことができず、利用するときはブロックを利用するメソッド(newやdefine_methodなど)の利用時に限られてしまいます。
このようなブロックの性質を補うのが、Procです。
Procは、ブロックをオブジェクトに変換します。
x = 1
y = 1
block = Proc.new { |z| (x + y) * z }
puts block.call(10) # => 20
Proc.newにブロックを渡すと、そのブロックのオブジェクトを作成します。
上記のコードでは、変数blockにブロックのオブジェクトを格納しています。
出力するときは、callメソッドで呼び出します。
call()の中身は作成したブロックの引数です。
ブロック定義時に||の中身をセットすると、それが引数として扱われます。
上記のコードの中では、引数に10がセットされ、ブロック内の|z|が|10|に変化します。
Procをインスタンスメソッドに渡す場合は、ブロックをメソッドに渡してから、以下ようにメソッド定義時に&をつけた引数を記入します。
x = 1
y = 1
def output(&block)
block.call * 10
end
puts output { x + y } # => 20
&はブロックをオブジェクトに変換します。つまり、上のコードの場合、ブロックをオブジェクトに変換し、それをblockという変数の中に格納しているわけです。
メソッド内には&を取り除いた引数をcallで呼び出して、ブロックを展開しています。
もちろん、すでにProcとなっているブロックを渡すこともできます。
x = 1
y = 1
def output(&block)
block.call * 10
end
block = Proc.new { x + y }
puts output &block # => 20
また、&はオブジェクトをブロックに変換することもできます。
したがって、以下のコードも可能です。
x = 1
y = 1
def output
# ブロックが引数に入ってきているので、yieldを使う。
yield * 10
end
block = Proc.new { x + y }
# Procをブロックに変換して、メソッドに渡す。
puts output(&block) # => 20
Lambda
Lambdaには字面的に難解な雰囲気が漂いますが、Procの色違いのようなものです。 挙動が少しProcとは異なりますが、やることは基本同じことです。x = 1
y = 1
block = lambda { |num| (x + y) * num }
puts block.call(10) # => 20
また、以下は上記のコードと同じように振る舞います。
x = 1
y = 1
block = ->(num) { (x + y) * num }
puts block.call(10) # => 20
x = 1
y = 1
block = -> { (x + y) * 10 }
puts block.call # => 20
Procと同様に、Lambdaの後ろにブロックを繋げ、必要であれば||に引数をセットすることができます。
callで呼び出し、ブロックの中に引数がセットされているのであれば、call(10)のようにして、引数に入れるものを指定できます。
Lambdaを引数としてメソッドに渡す場合は、以下のコードを参考にしてください。
基本的にProcと同じです。
# ブロックをオブジェクトに変換するだけならProcとまるっきり同じ。
# 引数の&は、ブロックをオブジェクトに変換し、変数blockに格納したと考えれば良い。
x = 1
y = 1
def output(&block)
block.call * 10
end
puts output { x + y } # => 20
x = 1
y = 1
def output(&block)
block.call * 10
end
block = lambda { x + y }
puts output &block
x = 1
y = 1
def output
yield * 10
end
block = lambda { x + y }
puts output(&block)
ProcとLambdaの違い
ProcとLambdaはブロックをオブジェクトにしたもので、両者はProcクラスに分類されるという点で同等です。proc = Proc.new { |x| x * 10 } # => ProcクラスのProc
lambda = lambda { |x| x * 10 } # => ProcクラスのLambda
puts proc.class # => Proc
puts lambda.class # => Proc
puts proc.lambda? # => False
puts lambda.lambda? # => True
LambdaはProcの色違いのようなものだと説明しましたが、それは、Procに属する毛色の違うProcである、というわけです。
反対に、Procに属するLambdaではないProcは、どう頑張ってもLambdaではありません。Lambdaになるのは、lambda{}と->{}の記法で作成されたProcのみです。lambdaかどうかを判断するには、lambda?メソッドが役に立ちます。
しかしながら、より厳密に言えば、それぞれは異なる挙動を行います。
ProcとLambdaの相違点を見分けるのはややこしいですし、例外もさまざまあるようなので、完全に理解する必要はないと思いますが、代表的な挙動の違いとして、returnの扱い方と、引数の扱い方の違いを説明します。
まずはそれぞれのreturnに対する挙動を確認します。
# Procの場合
def proc_output
block = Proc.new { return 100 }
called = block.call # => ブロックのスコープにあるreturnによってproc_outputの戻り値を返す
return called * 10
end
# Lambdaの場合
def lambda_output
block = lambda { return 100 }
called = block.call
return called * 10 # => メソッドのスコープにあるreturnによってlambda_outputの戻り値を返す
end
puts proc_output # => 100
puts lambda_output # => 1000
上記のコードから、
Procのブロック内にあるreturnは、callで評価された時に、メソッドのスコープを抜け出しますが、
Lambdaのブロック内にあるreturnは、callで評価された後、メソッドのスコープを抜け出さず、変数に格納され、メソッドのreturnによって戻り値になり、メソッドのスコープを抜け出しています。
もう1つのProcとLambdaの相違点は、引数の取り扱い方の違いにあります。
# Procの場合
proc = Proc.new { |x| x * 10 }
puts proc.call(5) # => 50
puts proc.call(5, 10) # => 50
# Lambdaの場合
lambda = lambda { |x| x * 10 }
puts lambda.call(5) # => 50
puts lambda.call(5, 10) # => wrong number of arguments (given 2, expected 1)
Procでは、余分に引数を渡していますが、正常に動いています。
対して、Lambdaでは、余分に引数を渡すとエラーが発生します。
Procは引数を多めに渡しても、少なめに渡しても、エラーを発生させることなく動いてくれます。
余分な引数は切り捨て、足りない分はnilとして許容するわけです。
しかし、Lambdaはそれを許しません。
より厳密に引数を受け取りたがります。
おさらいすると、ProcとLambdaには以下のような相違点があります。
- returnの挙動が異なる
・Procでは、returnを行うと、メソッドを抜けて、returnの値を戻す。
・Lambdaでは、returnを行うと、returnした値をロジックに組み込んでメソッドの最後まで計算する。 - 引数に対する厳格さ
・Procでは、設定した引数の数に対して渡す引数の数は幾つでもエラーにならない。
・Lambdaでは、設定した引数の数に対して、厳密な数の引数を対応させて渡さなければエラーになる。
このようなエラーがあるわけですが、どちらかというとLambdaを使った方が良いみたいです。
理由はProcのように引数に対して寛容すぎると、ヒューマンエラーで間違った数の引数を渡した時に、画面上ではエラーにならないので、そのエラー自体に気づきにくくなってしまうからです。
間違った時にはしっかりとエラーを吐いてくれるLambdaを利用した方が、プログラミングする側からすると、楽だとおもいます。