Ruby の return に注意しながら next と break をうまく使い分ける
はじめに
いきなりですが、TypeScript で以下のような関数を書いてみました。実行結果はどうなるでしょうか。 code:sample1.ts
function sample1(array: number[]): { result: string[] } {
const result = array.map((num) => {
if (num % 2 === 0) return ${num} は偶数
if (num % 2 === 1) return ${num} は奇数
return ${num} は偶数でも奇数でもない
})
return { result }
}
console.log(sample1(Array.from({length: 10}, (v, i) => i)))
実行結果は以下のようになります。resultをキーとした、文字列配列(10件分)のオブジェクトが入っていますね。
{
"result": [
"0 は偶数",
"1 は奇数",
"2 は偶数",
"3 は奇数",
"4 は偶数",
"5 は奇数",
"6 は偶数",
"7 は奇数",
"8 は偶数",
"9 は奇数"
]
}
Ruby で書いてみる(失敗例 return 編)
さて、これをそのまま Ruby に置き換えて書いてみました。しかし、一つ大きなミスを犯しています。どこが間違いかわかるでしょうか。また、実行結果はどうなるでしょうか。ちなみに、本記事の Ruby バージョンは3.1.2を使用しています。 code:sample1.rb
def sample1(array)
result = array.map do |num|
return "#{num} は偶数" if num % 2 == 0
return "#{num} は奇数" if num % 2 == 1
"#{num} は偶数でも奇数でもない"
end
{ result: }
end
p sample1(10.times)
実行結果は以下のようになります。
"0 は偶数"
この実行結果を見るとわかることとして、10件分の文字列配列が出力されておらず、引数に渡した文字列配列の1件目のデータである 0 が map ブロックの引数に渡され、 return "#{num} は偶数" が呼ばれたところで処理が終わっています。つまりreturn句を関数内・ブロック内で呼び出した場合は、その時点でブロックと関数を中断してreturnに渡された値を返します。
Ruby で書いてみる(失敗例 break 編)
Ruby のreturn句はブロックも関数も抜けてしまうので、breakで書き直してみます。以下の実行結果はどうなるでしょうか。
code:sample2.rb
def sample2(array)
result = array.map do |num|
break "#{num} は偶数" if num % 2 == 0
break "#{num} は奇数" if num % 2 == 1
"#{num} は偶数でも奇数でもない"
end
{ result: }
end
p sample2(10.times)
実行結果は以下のようになります。
{:result=>"0 は偶数"}
先ほどとは違い、resultをキーとしたハッシュオブジェクトが出力されるようになりました。ただ、値は相変わらず0は偶数という文字列のみです。つまり、break句を関数内・ブロック内で呼び出した場合は、ブロックを中断してbreakに渡された値を返しますが、関数は引き続き実行されます。中断する範囲がreturn句とは異なることに加えてbreak句が値を返すことができるというのも特徴的なポイントと言えます。
Ruby で書いてみる(成功例 next 編)
break句で一歩前進したものの、依然としてresultの値が文字列配列になっていないのでnextで書き直してみました。
code:sample3.rb
def sample3(array)
result = array.map do |num|
next "#{num} は偶数" if num % 2 == 0
next "#{num} は奇数" if num % 2 == 1
"#{num} は偶数でも奇数でもない"
end
{ result: }
end
p sample3(10.times)
実行結果は以下の通りです。
{:result=>["0 は偶数", "1 は奇数", "2 は偶数", "3 は奇数", "4 は偶数", "5 は奇数", "6 は偶数", "7 は奇数", "8 は偶数", "9 は奇数"]}
ようやくresultの値が10個の文字列配列をもつハッシュオブジェクトになりました。この実行結果を見るとわかることとしてnext句はブロックのループ処理は中断せず、呼ばれた際にブロック内部の後続処理をスキップします。また、breakと同様にnextも値を返すことができます。
まとめ
ここでわかったことをまとめます。
return: ブロックのループ処理も関数も中断して値を返す
break: ブロックのループ処理を中断して値を返す
next: ブロック内のループ処理結果として値を返し、次のループを開始する(ループ処理は中断されない)
さまざまな言語に触れていると、このような微妙な条件の違いに混乱し時に思わぬ結果をもたらします。このようなミスを犯さないように、実務ではテストコードを書いていくことが大切です。
余談: ラベルについて
冒頭のTypeScriptの例で言えば、breakやcontinueに渡せるものとしてラベルがあります。ラベルを使うことで、どの範囲のブロックを中断するかを明示的に指定することができます。TypeScriptのラベルについてはこちらを参照してください。 Rubyには、breakやnext句にラベルを使って中断先を明示的に指定することはできませんが、代わりにcatchとthrowを組み合わせて似たようなことができます。
code:label.rb
catch (:done) do
5.times { |i|
5.times { |j|
throw :done if i + j > 5
}
}
end
あまりこのようなコードを目にする機会は多くないかもしれませんが、覚えておくと良いかもしれません。