RubyのブロックとProcとLambdaについて
Rubyをはじめると、ブロックとかProcとかLambdaとか、 みんな似たような雰囲気をしているから、うまく理解しづらいですよね。
復習も兼ねて、余計な説明抜きで、ブロック・Proc・Lambdaについて説明していきます。
ブロック
ブロックはクロージャです。 ですので、ブロックを使うことで、普段参照できないローカル変数を参照できるようになります。
# block.rb 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もブロックのロジックを実行します。
# block.rb 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
また、ブロック内のロジックが一文だけなら、
# block.rb # each array.each { |num| puts x + num } # map array.map { |num| puts num * x }
このように記述すると読みやすいかもしれません。
ブロックはインスタンスメソッドに渡して、メソッド内で利用することもできます。
# block.rb 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は、ブロックをオブジェクトに変換します。
# proc_statement.rb 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をインスタンスメソッドに渡す場合は、ブロックをメソッドに渡してから、以下ようにメソッド定義時に&をつけた引数を記入します。
# proc_statement.rb x = 1 y = 1 def output(&block) block.call * 10 end puts output { x + y } # => 20
&はブロックをオブジェクトに変換します。つまり、上のコードの場合、ブロックをオブジェクトに変換し、それをblockという変数の中に格納しているわけです。 メソッド内には&を取り除いた引数をcallで呼び出して、ブロックを展開しています。 もちろん、すでにProcとなっているブロックを渡すこともできます。
# proc_statement.rb x = 1 y = 1 def output(&block) block.call * 10 end block = Proc.new { x + y } puts output &block # => 20
また、&はオブジェクトをブロックに変換することもできます。 したがって、以下のコードも可能です。
# proc_statement.rb x = 1 y = 1 def output # ブロックが引数に入ってきているので、yieldを使う。 yield * 10 end block = Proc.new { x + y } # Procをブロックに変換して、メソッドに渡す。 puts output(&block) # => 20
Lambda
Lambdaには字面的に難解な雰囲気が漂いますが、Procの色違いのようなものです。 挙動が少しProcとは異なりますが、やることは基本同じことです。
# lambda_statement.rb x = 1 y = 1 block = lambda { |num| (x + y) * num } puts block.call(10) # => 20
また、以下は上記のコードと同じように振る舞います。
# lambda_another_statement.rb x = 1 y = 1 block = ->(num) { (x + y) * num } puts block.call(10) # => 20
# lambda_another_statement.rb x = 1 y = 1 block = -> { (x + y) * 10 } puts block.call # => 20
Procと同様に、Lambdaの後ろにブロックを繋げ、必要であれば||に引数をセットすることができます。 callで呼び出し、ブロックの中に引数がセットされているのであれば、call(10)のようにして、引数に入れるものを指定できます。
Lambdaを引数としてメソッドに渡す場合は、以下のコードを参考にしてください。 基本的にProcと同じです。
# lambda_statement.rb # ブロックをオブジェクトに変換するだけならProcとまるっきり同じ。 # 引数の&は、ブロックをオブジェクトに変換し、変数blockに格納したと考えれば良い。 x = 1 y = 1 def output(&block) block.call * 10 end puts output { x + y } # => 20
# lambda_statement.rb x = 1 y = 1 def output(&block) block.call * 10 end block = lambda { x + y } puts output &block
# lambda_statement.rb x = 1 y = 1 def output yield * 10 end block = lambda { x + y } puts output(&block)
ProcとLambdaの違い
ProcとLambdaはブロックをオブジェクトにしたもので、両者はProcクラスに分類されるという点で同等です。
# difference.rb 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に対する挙動を確認します。
# difference.rb # 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の相違点は、引数の取り扱い方の違いにあります。
# difference.rb # 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には以下のような相違点があります。 1. returnの挙動が異なる ・Procでは、returnを行うと、メソッドを抜けて、returnの値を戻す。 ・Lambdaでは、returnを行うと、returnした値をロジックに組み込んでメソッドの最後まで計算する。 2. 引数に対する厳格さ ・Procでは、設定した引数の数に対して渡す引数の数は幾つでもエラーにならない。 ・Lambdaでは、設定した引数の数に対して、厳密な数の引数を対応させて渡さなければエラーになる。
このようなエラーがあるわけですが、どちらかというとLambdaを使った方が良いみたいです。 理由はProcのように引数に対して寛容すぎると、ヒューマンエラーで間違った数の引数を渡した時に、画面上ではエラーにならないので、そのエラー自体に気づきにくくなってしまうからです。 間違った時にはしっかりとエラーを吐いてくれるLambdaを利用した方が、プログラミングする側からすると、楽だとおもいます。