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
ブロックは do
と end
で書くこともできます:
(1..10).each do |n|
puts n
end
このような do
と end
のブロックは Body と呼ばれたりもします.
ブロックはこのようにメソッドの引数として渡されて初めて機能するので, ブロック単体では機能しません.
実際にメソッドの引数として渡し, メソッド内で呼び出してみたいと思います:
def foo(&blk)
blk.call
end
foo { 'Inside a block' } # => "Inside a block"
このようにメソッドを定義する時に &blk
と &
を付けてパラメータを指定すると, 与えられたブロックをその &blk
を通じて参照することができます.
ただ &blk
とブロックを受け取るパラメータを指定する場合, 他のどのパラメータよりも一番最後に指定する必要があります.
そして渡された &blk
を blk.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.new
や proc
で, Lambda は lambda
や ->
をまず書いてあげて , ブロック {}
を付けてあげると作ることができます.
x
などのブロックのパラメータに関してはブロック内の ||
で囲まれた中に入れると指定できます.
->
による Lambda の作り方だけ他と比べると少し変則的ですが, これは Lambda リテラルと言われるもので, Lambda がより頻繁に使われることを想定されているためにこのようなリテラルがあるのでしょう.
->
でブロックパラメータを指定する場合は, ||
ではなく ()
の中で指定します.
また ()
はブロックの中ではなく, ->
のすぐ右に書き, ->()
のように ->
と ()
の間にスペースが入らないようにします.
また Proc と Lambda のブロックは {}
の代わりに do
と end
で書くこともできます:
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.new
や lambda
で定義された時のスコープの変数, メソッド, クラスへの参照を保存し, いろいろなコンテクストで呼ばれても, そのあらかじめ保存しておいた参照を使うのでコンテクストに影響されません:
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
が返す n
は n = 1
の時のまま変わらずということです.
ただ注意点としましては pr
は n = 1
の n
の参照を保存しているのであって, 1
という値を保存している訳ではありません:
n = 1
pr = Proc.new { n }
n = 2
pr.call # => 2
このように最初は n = 1
と定義した後に pr
を定義したとしても, その後に n = 2
と n
を代入し直してしまえば pr
が参照している n
の値も 1
から 2
に変わり, pr.call
で 2
を返します.
まとめ
ブロックは単体では使えず, メソッドの引数として渡されます.
渡されたブロックはメソッドの中で 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 の違いを理解する一助になりましたら幸いです.
最後までお読みくださいましてありがとうございました.
関連記事
Ruby の関数プログラミングでオイラー積を計算してみた2018.07.02
Ruby の正規表現を備忘録としてまとめてみた2018.08.30
Ruby の StandardError でどういう間違いをすると, どの例外が発生するのかのメモ2018.07.13
Ruby でフィボナッチ数を計算する方法2018.06.23
Ruby のグローバル変数をそれぞれ調べてみた2018.06.12