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を利用した方が、プログラミングする側からすると、楽だとおもいます。