はじめに
RubyではProc.new
(またはproc
メソッド)で手続きオブジェクトを作成することができます。
f = Proc.new { 'Hello!' }
# または
# f = proc { 'Hello!' }
f.call #=> "Hello!"
手続きオブジェクトはラムダ構文(またはlambda
メソッド)を使って作成する方法もあります。
f = -> { 'Hello' }
# または
# f = lambda { 'Hello' }
f.call #=> "Hello!"
ここでは便宜上、前者をProc.new、後者をラムダと呼ぶことにします。
Proc.newもラムダもどちらもProcクラスのインスタンスですが、微妙に挙動が違います。
1つは引数の扱いです。ラムダは引数の数を厳密にチェックします。
# Proc.newは引数の過不足を許容する
f = Proc.new { |s| "Hello, #{s}!" }
f.call('Alice') #=> => "Hello, Alice!"
f.call #=> "Hello, !"
# ラムダは引数の数が異なるとエラーになる
f = -> (s) { "Hello, #{s}!" }
f.call('Alice') #=> "Hello, Alice!"
f.call #=> ArgumentError: wrong number of arguments (given 0, expected 1)
もうひとつはProc.newやラムダの内部でreturnやbreakを呼びだした場合の挙動です。
この記事ではこの違いをまとめてみます。
おことわり
この記事は以下の公式ドキュメントの内容を独自の観点で再構成したものです。
説明内容に誤りがあればコメントや編集リクエストで優しく指摘してやってください。
同じメソッド内でreturnやbreakが呼ばれた場合
まず、手続きオブジェクトの作成と呼び出しが同じメソッド内で行われた場合の挙動を確認してみます。
returnが呼ばれた場合
最初はreturnが呼ばれた場合の違いを確認してみます。
ここでは配列のmap
メソッドにProcやラムダを渡し、手続きオブジェクト内でreturnを呼び出します。
比較のためにブロックの内部からreturnを呼びだした場合の実行結果も載せておきます。
def proc_return
f = Proc.new { |n| return n * 10 }
ret = [1, 2, 3].map(&f)
"ret: #{ret}"
end
def lambda_return
f = -> (n) { return n * 10 }
ret = [1, 2, 3].map(&f)
"ret: #{ret}"
end
def block_return
ret = [1, 2, 3].map { |n| return n * 10 }
"ret: #{ret}"
end
proc_return #=> 10
lambda_return #=> "ret: [10, 20, 30]"
block_return #=> 10
proc_return
とblock_return
は戻り値が数値の10になっています。
このことからreturnが呼ばれた時点で外側のメソッド(proc_return
やblock_return
)の実行が終了していることがわかります。
一方、lambda_return
は戻り値が"ret: [10, 20, 30]"
になっています。
このことから、外側のメソッド(lambda_return
)が最後まで実行されていることがわかります。
また、map
メソッドの戻り値が[10, 20, 30]
になっていることから、returnを呼んでもmap
メソッドのループ処理が中断されていないことがわかります。
つまり、このような結論になります。
- Proc.newやブロックの場合 = returnが呼ばれるとメソッドを抜ける
- ラムダの場合 = returnが呼ばれてもメソッドを抜けない。ループ処理も中断しない(手続きオブジェクト内の処理を抜けるだけ)
breakが呼ばれた場合
次はbreakを使ってみます。
returnの代わりにbreakを使っている点以外は先ほどのコードと同じです。
def proc_break
f = Proc.new { |n| break n * 10 }
ret = [1, 2, 3].map(&f)
"ret: #{ret}"
end
def lambda_break
f = -> (n) { break n * 10 }
ret = [1, 2, 3].map(&f)
"ret: #{ret}"
end
def block_break
ret = [1, 2, 3].map { |n| break n * 10 }
"ret: #{ret}"
end
proc_break #=> LocalJumpError: break from proc-closure
lambda_break #=> "ret: [10, 20, 30]"
block_break #=> "ret: 10"
breakの場合は3つとも結果が異なります。
Proc.newの場合は例外が発生しています。
ラムダはreturnと同じように外側のメソッドが最後まで実行されています。
また、map
メソッドの戻り値が[10, 20, 30]
になっていることから、breakを呼んでもmap
メソッドのループ処理が中断されていないことがわかります。
ブロックの場合も外側のメソッドは最後まで実行されていますが、map
メソッドの戻り値が10
になっています。
このことから、map
メソッドのループ処理が途中で中断されたことがわかります。
つまり、次のような結論になります。
- Proc.newの場合 = breakが呼ばれると例外が発生する
- ラムダの場合 = breakが呼ばれてもループ処理を中断しない(手続きオブジェクト内の処理を抜けるだけ)
- ブロックの場合 = breakが呼ばれるとループ処理を中断する
nextが呼ばれた場合
もうひとつ、nextが呼ばれた場合の挙動も確認しておきましょう。
def proc_next
f = Proc.new { |n| next n * 10 }
ret = [1, 2, 3].map(&f)
"ret: #{ret}"
end
def lambda_next
f = -> (n) { next n * 10 }
ret = [1, 2, 3].map(&f)
"ret: #{ret}"
end
def block_next
ret = [1, 2, 3].map { |n| next n * 10 }
"ret: #{ret}"
end
proc_next #=> "ret: [10, 20, 30]"
lambda_next #=> "ret: [10, 20, 30]"
block_next #=> "ret: [10, 20, 30]"
nextを使ったときはどれも同じ結果になりました。
すなわち、nextが呼ばれたタイミングで次のループ処理に移り、最後までループ処理を続けます。
例外も発生しません。
メソッド外部でreturnやbreakが呼び出された場合
次にProc.newやラムダを、オブジェクトを作成したメソッドの外部で呼びだしてみます。
returnが呼ばれた場合
Proc.newやラムダをメソッドの戻り値として返し、メソッドの外部で手続きを実行してみます。
最初はreturnを呼びだしてみます。
def proc_return
Proc.new { |n| return n * 10 }
end
def lambda_return
-> (n) { return n * 10 }
end
[1, 2, 3].map(&proc_return) #=> LocalJumpError: unexpected return
[1, 2, 3].map(&lambda_return) #=> [10, 20, 30]
Proc.newの場合は例外が発生しました。
一方、ラムダの場合は例外が発生せず、最後までmap
メソッドのループ処理が実行されています。
breakが呼ばれた場合
次にbreakを呼びだしてみます。
def proc_break
Proc.new { |n| break n * 10 }
end
def lambda_break
-> (n) { break n * 10 }
end
[1, 2, 3].map(&proc_break) #=> LocalJumpError: break from proc-closure
[1, 2, 3].map(&lambda_break) #=> [10, 20, 30]
こちらもProc.newの場合は例外が発生しました。
一方、ラムダの場合は例外が発生せず、最後までmap
メソッドのループ処理が実行されています。
(breakを呼びだしてもループ処理は中断しません)
nextが呼ばれた場合
最後にnextを呼びだした場合です。
def proc_next
Proc.new { |n| next n * 10 }
end
def lambda_next
-> (n) { next n * 10 }
end
[1, 2, 3].map(&proc_next) #=> [10, 20, 30]
[1, 2, 3].map(&lambda_next) #=> [10, 20, 30]
こちらはどちらも同じ結果になりました。
すなわち、nextが呼ばれたタイミングで次のループ処理に移り、最後までループ処理を続けます。
例外も発生しません。
まとめ:コーディングを工夫してreturnやbreakの使用を避けよう
ここまでの話を表でまとめると次のようになります。
同じメソッド内でreturnやbreakが呼ばれた場合
return | break | next | |
---|---|---|---|
Proc.new | メソッドを抜ける | 例外が発生する | 次のループ処理に移る |
ラムダ | メソッドを抜けない。 ループ処理も中断しない。 (手続きオブジェクト内の処理を抜けるだけ) |
ループ処理を中断しない (手続きオブジェクト内の処理を抜けるだけ) |
次のループ処理に移る |
ブロック | メソッドを抜ける | ループ処理を中断する | 次のループ処理に移る |
メソッド外部でreturnやbreakが呼び出された場合
return | break | next | |
---|---|---|---|
Proc.new | 例外が発生する | 例外が発生する | 次のループ処理に移る |
ラムダ | ループ処理を中断しない (手続きオブジェクト内の処理を抜けるだけ) |
ループ処理を中断しない (手続きオブジェクト内の処理を抜けるだけ) |
次のループ処理に移る |
・・・ややこしすぎますね😅
個人的な感想を言わせてもらえば、
Proc.newやラムダの中でわざわざreturnやbreakを使おうとするな!!
と言いたいです(苦笑)。
特殊な要件であればProc.newやラムダの中でreturnやbreakを使うと便利なケースもあるのかもしれませんが、たいていの場合はコーディングの工夫でreturnやbreakを使わず済ませることができるはずです。
この記事を読んだ方は、「よし、落とし穴にはまらないように気を付けながらreturnやbreakを使うぞ」と思うのではなく、「うかつにreturnやbreakを使うと、わかりにくい不具合の原因になりそうだな」と思うようにしてくださいね。
あわせて読みたい
この記事だけでなく、公式ドキュメントの内容もあわせてチェックしておきましょう。