Ruby のブロック, Proc, Lambda の違い


Ruby を使いこなす上で欠かせないブロック, Proc, Lambda の違いを紹介します.

どれも同じような印象を受けがちですが, それぞれに違いがあります.

願わくばこの記事をお読みいただいた後, それぞれの具体的な違いを理解していただけましたら幸いです.

ブロック

まずはブロックから説明したいと思います.

ブロックは each メソッドなどでよく使われていて馴染みのあるものではないかと思います:

(1..10).each { |n| puts n }

{} で囲まれた部分が Ruby のブロックとなります.

n はブロックのパラメータで (1..10) のレンジのそれぞれの要素が順番にその n に渡され puts n で次のように表示されます:

1
2
3
4
5
6
7
8
9
10

ブロックは doend で書くこともできます:

(1..10).each do |n|
  puts n
end

このような doend のブロックは Body と呼ばれたりもします.

ブロックはこのようにメソッドの引数として渡されて初めて機能するので, ブロック単体では機能しません.

実際にメソッドの引数として渡し, メソッド内で呼び出してみたいと思います:

def foo(&blk)
  blk.call
end

foo { 'Inside a block' }  # => "Inside a block"

このようにメソッドを定義する時に &blk& を付けてパラメータを指定すると, 与えられたブロックをその &blk を通じて参照することができます.

ただ &blk とブロックを受け取るパラメータを指定する場合, 他のどのパラメータよりも一番最後に指定する必要があります.

そして渡された &blkblk.call で呼び出し, "Inside a block" というブロック内での返り値が foo の返り値となりました. .

このように渡された &blk をメソッド内で呼び出す際は blk.call&blk から & を取る必要があります.

& 取ることによって blk というローカル変数を Proc オブジェクトとして扱うことができます:

def foo(&blk)
  blk.class
end

foo {} # => Proc

このように {} というブロックがローカル変数 blk になって, Proc オブジェクトに変換されているのがわかります.

つまりはブロックがメソッドに渡されると, メソッド内で Proc オブジェクトに変化するんです.

ブロックにパラメータを指定し, メソッド内で呼び出すときに引数を渡すこともできます :

def foo(&blk)
  blk.call('Inside a method')
end

foo { |x| x }  # => "Inside a method"

また渡されるブロックは, メソッドを定義する時の一番最後のパラメータに &blk などと指定して受け取ることができますが, &blk と指定しなくてもブロックが渡されればメソッド内から yield で呼び出すことができます:

def foo
  yield
end

foo { 'Inside a block' }  # => "Inside a block"

call と同じように yield に引数を渡すこともできます:

def foo
  yield 'Inside a method'
end

foo { |x| x} # => "Inside a method"

このようにブロックを呼び出す際は &blk.call で明示的に呼び出すことも, yield で暗黙的に呼び出すことも可能です.

ただ yield でブロックを暗黙的に呼び出す場合, ブロックが渡されたという前提が必要ですので, ブロックが渡されたかどうか確認する必要がある場合, その確認するためのメソッドとして block_given? があります:

def foo
  return 'No block was given' unless block_given?
  yield 'A block was given'
end

foo           # => "No block was given"
foo { |x| x } # => "A block was given"

Proc と Lambda

Proc と Lambda は似ているので, 一緒紹介させていただきたいと思います.

事実 Lambda は Proc と同じ Proc オブジェクトです.

それぞれの作り方は次のようになります:

# Create procs
p1 = Proc.new { |x| puts x }
p2 = proc { |x| puts x }

# Create lambdas
l1 = lambda { |x| puts x }
l2 = ->(x) { puts x }

Proc は Proc.newproc で, Lambda は lambda-> をまず書いてあげて , ブロック {} を付けてあげると作ることができます.

x などのブロックのパラメータに関してはブロック内の || で囲まれた中に入れると指定できます.

-> による Lambda の作り方だけ他と比べると少し変則的ですが, これは Lambda リテラルと言われるもので, Lambda がより頻繁に使われることを想定されているためにこのようなリテラルがあるのでしょう.

-> でブロックパラメータを指定する場合は, || ではなく () の中で指定します.

また () はブロックの中ではなく, -> のすぐ右に書き, ->() のように ->() の間にスペースが入らないようにします.

また Proc と Lambda のブロックは {} の代わりに doend で書くこともできます:

p1 = Proc.new do |x|
  puts x
end

p2 = proc do |x|
  puts x
end

l1 = lambda do |x|
  puts x
end

l2 = ->(x) do
  puts x
end

そして定義された Proc と Lambda は call メソッドで呼び出すことができます:

p1.call('Proc 1')   # Proc 1
p2.call('Proc 2')   # Proc 2
l1.call('Lambda 1') # Lambda 1
l1.call('Lambda 1') # Lambda 2

もしくは [], .(), yield でも同じように呼び出すこともできます:

p1.call('Proc 1')    # Proc 1
p1['Proc 1']         # Proc 1
p1.('Proc 1')        # Proc 1
p1.yield('Proc 1')   # Proc 1

p2.call('Proc 2')    # Proc 2
p2['Proc 2']         # Proc 2
p2.('Proc 2')        # Proc 2
p2.yield('Proc 2')   # Proc 2

l1.call('Lambda 1')  # Lambda 1
l1['Lambda 1']       # Lambda 1
l1.('Lambda 1')      # Lambda 1
l1.yield('Lambda 1') # Lambda 1

l2.call('Lambda 2')  # Lambda 2
l2['Lambda 2']       # Lambda 2
l2.('Lambda 2')      # Lambda 2
l2.yield('Lambda 2') # Lambda 2

.()call メソッドのシンタクティクシュガーなので, Proc オブジェクトに限らず call メソッドが定義されているオブジェクトであれば同じように .()call の代わりとして使うこともできます:

class String
  def call
    puts self
  end
end

'Hello World'.call # Hello World
'Hello World'.()   # Hello World

とここまでは Proc と Lambda 共に違いは見られないですが, call で引数をブロックパラメータに渡した時と, ブロック内で return した時に両者に違いが出てきます.

引数を渡した時

Proc の方は Proc を定義する際に指定したパラメータの数に対して異なる数の引数が call に与えられて呼び出されたとしてもエラーを出しません:

pr = Proc.new { |a, b| [a, b]}
pr.call(1, 2)     # => [1, 2]
pr.call(1)        # => [1, nil]
pr.call(1, 2, 3)  # => [1, 2]
pr.call([1, 2])   # => [1, 2]

指定したパラメータに与えられなかった引数は nil が暗黙的に与えられます.

反対に指定したバラメータの数より大く与えられた余分な引数は, 無視されます.

また引数として Array を与えられた場合, Array のそれぞれの要素はそれぞれの引数として解釈されます.

Lambda の方は指定したパラメータの数と同じ数の引数を渡さないとエラーを出します:

lm = lambda { |a, b| [a, b] }
lm.call(1, 2)    # => [1, 2]
lm.call(1)       # Raise ArgumentError
lm.call(1, 2, 3) # Raise ArgumentError
lm.call([1, 2])  # Raise ArgumentError

このように Proc は引数の渡し方に柔軟性がありますが, Lambda は正確に指定したパラメータと同じ数の引数を渡さなければならず厳格です.

return した時

今度は return を Proc と Lambda それぞれのブロック内に記述した際の違いについて説明します.

Proc の場合

Proc の場合, ブロック内で記述された return はその Proc が定義された時のスコープの return になります.

つまりは定義された時のスコープ次第でどこから return されるのかということが変化します.

メソッド内で Proc が定義されれば, そのメソッド内の return ということになりますので, call より下のスクリプトが実行されません:

def foo
  pr = Proc.new { return }
  pr.call
  'I am not retuned'
end

foo # => nil

他のメソットで定義された Proc を別のメソッドで呼び出すと, 定義された時のメソッドのスコープの return となるので LocalJumpError が発生します:

def foo
  Proc.new { return }
end

def bar(pr)
  pr.call
end

bar(foo) # Raise LocalJumpError

ローカル空間で定義された Proc の return はローカル空間での return になるので , メソッド内で call された場合, call より下のスクリプトが実行されないことに加えて, その定義されたメソッドより下のスクリプトも実行されません:

pr = Proc.new { return }

def foo(pr)
  pr.call
  puts 'I am not putted'
end

foo(pr)
puts 'I also am not putted'

このように Proc のブロック内の return は Proc が Proc.new で定義された時のスコープでの return になり, 少しトリッキーなので注意が必要です.

Lambda の場合

Lambda の場合, ブロック内で記述された return はそのブロック内からの return になります.

なのでどのスコープで Lambda を定義しようと return は常に Lambda のブロックからの return になります:

def foo
  la = lambda { return }
  la.call
  'I am retuned'
end

foo # => "I am retuned"

Lambda の場合, メソッドに似ているんですよね.

一方の Proc の場合は, メソッドのブロックに似ているんです.

& でブロックにした時

Proc と Lambda は & を頭につけることによってブロックとしてメソッドの引数として渡すことができます.

その場合, & がついた引数は一番最後の引数である必要があります:

total = proc { |x, y| x + y }
(1..10).reduce(0, &total)  # => 55

上記のコードは, ブロックで書くと次のように書かれます:

(1..10).reduce(0) { |x, y| x + y }

このように &total& つけることによって Proc をブロックとして扱うことができます.

Lambda も同じように & つけるとブロックとして扱えます.

Proc と Lambda どちらも & をつけることによってブロックとしてメソッドの引数に渡すことができますが, Proc に & を付けて渡すとメソッド内では Proc になり, Lambda に & を付けて渡すとメソッド内では Lambda になります:

def foo(&b)
  b.lambda?
end

foo &proc {}    # => false
foo &lambda {}  # => true

なので Proc と Lambda 両者の違いである渡されるブロックパラメータの数に柔軟性があるのかそれとも厳格であるのかや, ブロック内の return の挙動の違いも, メソッド内で Proc オブジェクトに変換されてもそのまま保持されます.

ちなみにただのブロックを渡した場合は, そのブロックはメソッド内では Proc となります:

def foo(&b)
  b.lambda?
end

foo {}  # => false

クロージャ

Proc と Lambda は Ruby ではクロージャとも呼ばれています.

Proc と Lambda 両者とも Proc.newlambda で定義された時のスコープの変数, メソッド, クラスへの参照を保存し, いろいろなコンテクストで呼ばれても, そのあらかじめ保存しておいた参照を使うのでコンテクストに影響されません:

def foo(pr)
  n = 2
  pr.call
end

n = 1
pr = Proc.new { n }

foo(pr)  # => 1

みていただくとわかるように, foo メソッド内で n = 2 としたにも関わらず pr.call の返り値, ひいては foo(pr) の返り値は 1 です.

これがクロージャの特徴で, pr が定義された時のスコープの n の参照を保存しているので, foo メソッド内でいくら同じ名前の n が定義されたとしても, それはあくまでメソッド内というまた違ったスコープの n なので, n = 2 としても pr.call が返す nn = 1 の時のまま変わらずということです.

ただ注意点としましては prn = 1n の参照を保存しているのであって, 1 という値を保存している訳ではありません:

n = 1
pr = Proc.new { n }

n = 2
pr.call  # => 2

このように最初は n = 1 と定義した後に pr を定義したとしても, その後に n = 2n を代入し直してしまえば pr が参照している n の値も 1 から 2 に変わり, pr.call2 を返します.

まとめ

ブロックは単体では使えず, メソッドの引数として渡されます.

渡されたブロックはメソッドの中で Proc に変換されます.

Proc と Lambda 共に Proc オブジェクトですが, Proc の方はブロックパラメータに与えられる引数の数に対して柔軟性があるのに対し, Lambda の方は正確でないとなりません.

ブロック内に記述された return は Proc の方は, 定義された時のスコープに依存しますが, Lambda の方は常にブロックからの return になります.

Proc と Lambda は頭に & をつけることによってメソッドのブロックとして渡すことができます.

& を付けて渡したとしてもメソッド内で Proc オブジェクトに変換される時に, もともと Proc だったら Proc に, もともと Lambda だったら Lambda に変換されます.

Proc と Lambda はそれぞれクロージャと呼ばれるもので, 定義された時のスコープに定義されている変数への参照を保存するので, 色々なスコープで後から call で呼ばれようとも最初に定義された時のスコープしか使用せず, それ以外のスコープの影響は受けません.

そんな感になります.

なのでブロック, Proc, Lambda ってどれも全く別々というものではなく, むしろ同類のようなものです.

それゆえに僕は少し混乱してしまうところがありましたので, 同じように少し混乱されている人がいるのではないかということで, そのような混乱を払拭することができたらいいなと今回はこのような内容を書かせていただきました.

Ruby のブロック, Proc, Lambda の違いを理解する一助になりましたら幸いです.

最後までお読みくださいましてありがとうございました.