68
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

returnやbreakを使ったときのProc.newとラムダの挙動の違い

Last updated at Posted at 2017-03-30

はじめに

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_returnblock_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を使うと、わかりにくい不具合の原因になりそうだな」と思うようにしてくださいね。

あわせて読みたい

この記事だけでなく、公式ドキュメントの内容もあわせてチェックしておきましょう。

手続きオブジェクトの挙動の詳細

68
49
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
68
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?