bonar note

京都のエンジニア bonar の技術的なことや技術的でない日常のブログです。

ruby ブロック内からの return

例えば受け取った配列内を一つずつ見ていって偶数の場合のみ puts する、という処理をしようとして以下のようなコードを書いたとします。

def print_evens(nums)
  nums.each do |num|
    return if 1 == (num % 2)
    puts num
  end
end

print_evens((1..10).to_a)

each に与えられたブロックの最初の行で奇数の場合はブロックの処理を終え(次の num の実行に移り)、そうじゃない場合(偶数の場合)には puts num するという意図ですが、このコードを実行しても何も表示されません。

ブロック内の return で print_evens メソッドそのものを抜けてしまうからです。
意図した動きにするためには return を next に変える必要があります。

def print_evens(nums)
  nums.each do |num|
    next if 1 == (num % 2)
    puts num
  end
end

print_evens((1..10).to_a)
2
4
6
8
10

break にした場合には each のループのみを抜けて print_evens は抜けず、次の文に制御が移ります。

驚くほど当たり前の事を書いている感じなのですが、JavaScript のコードと行き来しているとこういうミスをしてしまいがちで、特に文法エラーになるわけでもないので分かりにくいバグとして潜伏しがちかなと思いました。

また、next が値を受け取れるという事は見逃されがちです。これはとても便利に使えて、例えば fizzbuzz を以下のように書けます。

def multiple?(a, b)
  !!(0 == (a % b))
end

def fizzbuzz(nums)
  nums.map do |num|
    next "fizzbuzz" if multiple?(num, 3) && multiple?(num, 5)
    next "fizz" if multiple?(num, 3)
    next "buzz" if multiple?(num, 5)
    next num
  end
end

puts fizzbuzz((1..20).to_a)

ブロック内で最後に評価される値を意図した物にしようとしてネストが深くなったり複雑化するよりは、next で早めに返す方が見通しがいい、という局面は結構ありそうですね。