😳

【Ruby】 do...end と {...}の違いでハマった

2024/01/13に公開

Railsでテストを書いていたところ、do...end{...}で挙動が異なり、ハマってしまいました。

書いたコード

配列のすべての要素が条件を満たしていることのテストを書いていました。
サンプルコードは以下です。

# テストに成功する(偽陰性)
assert [1, 2, -1].all? do |num|
  num.positive?
end

こちらは落ちるべきテストです。
しかし、このテストは成功してしまいます。(偽陰性)

しかし、 do...endから{...}に書き換えると、テストは正しく失敗してくれます。

# テストに失敗する
assert [1, 2, -1].all? { |num|
  num.positive?
}

結合度の違い

do..end{...}は結合度というものが異なり、{...}の方が結合が強いです。

結合度とは、ブロックを渡したときに、どのメソッドの引数として認識されるかの違いです。
公式リファレンスでは以下のサンプルコードが書かれています。

foobar a, b do body end   # foobarの引数はa, bの値とブロック
foobar a, b { body }      # ブロックはメソッドbの引数、aの値とbの返り値とがfoobarの引数

つまり、do...endは先頭のメソッドの引数になり、{...}は直前のメソッドの引数になるということでしょうか。(詳細に調べていないので断言できませんが)

前述のテストコードでは、do...endがassertメソッドの引数として渡されたため、テストが通ってしまいました。 {...}で書くとブロックはall?メソッドに渡されます。

どう使い分けるか

公式リファレンスでは以下のように述べられています。

{ ... } の方が do ... end ブロックよりも強く結合します。次に例を挙げますが、このような違いが影響するコードは読み辛いので避けましょう:

私も同意で、ここはコーディングやレビュー時に気づくのは難しそうだと思いました。

対応方法

RubocopのAmbiguousBlockAssociationで検知できるようです。
メソッドの引数を()で囲い、どのメソッドへの引数かを明示することで修正できます。

RuboCop 0.48から導入されたようなので、バージョンが低い場合は検出されない可能性がありますのでご注意ください。

Discussion