LoginSignup
9
0

More than 3 years have passed since last update.

RuboCopの Style/DoubleNegation に抵触しないboolean変換

Last updated at Posted at 2019-08-30

Rubyのイディオムで、真偽値と見た任意のオブジェクト obj1booleanである true または false に変換する方法として、二重否定 !!obj がある。

しかしRubyのスタイルガイドはこれを勧めていない。実際にやってみると、以下のコードはRuboCopのチェックにひっかかる。

to_bool.rb
# frozen_string_literal: true

def to_bool(obj)
  !!obj
end

if $PROGRAM_NAME == __FILE__
  ary = [true, false, nil, 0, '', BasicObject.new]
  p ary.map(&method(:to_bool))
  #=> [true, false, false, true, true, true]
end
$ ruby -v
ruby 2.6.0p0 (2018-12-25 revision 66547) [x86_64-linux]
$ rubocop -v
0.74.0

$ rubocop to_bool.rb
Inspecting 1 file
C

Offenses:

to_bool.rb:4:3: C: Style/DoubleNegation: Avoid the use of double negation (!!).
  !!obj
  ^

1 file inspected, 1 offense detected

Rubyのスタイルガイドの言い分は以下の通り。

https://rubystyle.guide/#no-bang-bang

Double Negation

Avoid the use of !!.

!! converts a value to boolean, but you don’t need this explicit conversion in the condition of a control expression; using it only obscures your intention. If you want to do a nil check, use nil? instead.

「条件式で使う必要は無い」ということで、その考えは私も賛成する。一方で、例えばAPIレスポンスの要素で true / false を返したいとか、真偽以外の情報を消すためbooleanに変換したいことはある。

RuboCop自体を変えるかの議論は上に任せるとして、ルールで許可するのは普通過ぎてつまらないので、監視をすり抜ける方法を探ってみた。

注:結果が異なる例

RuboCopのスタイルガイドに書いてある方法

https://docs.rubocop.org/en/stable/cops_style/#styledoublenegation

Style/DoubleNegation

This cop checks for uses of double negation (!!) to convert something to a boolean value. As this is both cryptic and usually redundant, it should be avoided.

Please, note that when something is a boolean value !!something and !something.nil? are not the same thing. As you're unlikely to write code that can accept values of any type this is rarely a problem in practice.

Examples

# bad
!!something

# good
!something.nil?

例を真似るとこうなる。

  !obj.nil?

ガイドに注記がある通り、あらゆるオブジェクトに対しては使えない。例えば、

  • obj = false だと結果が true になってしまう
  • BasicObject のインスタンスだと NoMethodError を引き起こす

捕まる例

スペースを入れる

!! と連続しているのを捉えているなら、間を空ければごまかせそう。

  ! !obj

バレた。しかも ! の後にスペースを入れたのを注意された。

to_bool.rb:4:3: C: Layout/SpaceAfterNot: Do not leave space between ! and its argument.
  ! !obj
  ^^^^^^
to_bool.rb:4:3: C: Style/DoubleNegation: Avoid the use of double negation (!!).
  ! !obj
  ^

BasicObject#! を使う

単項演算子だからダメなだけで、メソッドとして呼び出せば見逃がされるかもしれない。

  !obj.!

これもバレた。きちんと単項演算子と同じに見えているらしい。

to_bool.rb:4:3: C: Style/DoubleNegation: Avoid the use of double negation (!!).
  !obj.!
  ^

not を組み合わせる

! と優先順位が異なる演算子なので、連続しているとは判定されないはず。

  not !obj

確かに連続しているとは判定されなかったが、 not の使用を咎められた。

to_bool.rb:4:3: C: Style/Not: Use ! instead of not.
  not !obj
  ^^^

すり抜けられる例

#__send__ で間接的に否定する

直接 #! を呼び出すのはダメだったが、間接的なら気付かれない。 #__send__ は BasicObject に定義されていて、再定義すべきでないともされているので、全てのオブジェクトで使えると考えていい。

  !obj.__send__(:!)

if式を使う

あからさまに冗長だが、したいことは明確。これは咎められない。

  if obj
    true
  else
    false
  end

ただし1行で書くと引っかかる。

  if obj then true else false end
to_bool.rb:4:3: C: Style/OneLineConditional: Favor the ternary operator (?:) over if/then/else/end constructs.
  if obj then true else false end
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

三項演算子を使う

というわけで、言われた通り三項演算子を使う。楽だしわかりやすい。

  obj ? true : false

論理演算子(制御構造)を使う

&&|| の短絡評価の性質を利用する。残念ながら三項演算子より長いしわかりにくい。

  obj && true || false

論理否定を1回のみにする

! 1回で既にbooleanに変換されているので、あとは truefalse と比較すればいい。短くなったが更にわかりにくい。

  !obj == false
  !obj != true

Enumerableのメソッドを使う

リストの全要素の真偽を確かめられるメソッドがいくつかある。 obj を長さ1の配列に入れて検査すればいい。

  [obj].all?
  [obj].any?
  [obj].one?

ただし #none? だけはうまくいかない。論理否定と組み合わせる必要があり、それならより簡潔なメソッドに置き換えられるため。

  ![obj].none?
to_bool.rb:4:3: C: Style/InverseMethods: Use any? instead of inverting none?.
  ![obj].none?
  ^^^^^^^^^^^^

Object#itself を挟むとバレなくなり、他の規則にも引っかからない。

  ![obj].none?.itself

論理演算子(メソッド)を使う

1回の演算で済む方法。制御構造のほうはオブジェクトをそのまま返しうるのに対し、これらのメソッドは(組み込みクラスの動作を書き換えない限り) truefalse を返す。前節の方法を配列長1ということに注目して最適化したともとれる。

  true & obj

  false | obj
  nil | obj

  false ^ obj
  nil ^ obj

括弧で区切る

試していたら、これはOKだった。構文解析で ! が連続していないことになっているのだろうか?

  !(!obj)

知見

RuboCopの監視をすり抜けられるboolean変換には、以下のような方法が見つかった。

  • !(!obj)括弧を使うだけでごまかせる(意外!)
  • nil | obj など論理演算子メソッドでも短く書けて、地味に演算回数も少なくなる
    • [obj].any? などEnumerableのメソッドもかなり短いが、論理演算子メソッドには劣る
  • obj ? true : false などと普通に条件分岐するのが一番わかりやすく、メソッド書き換えの影響も一切受けない

細かいことを言うと、これらはメソッド書き換えを考慮すると等価ではない。外部のコードを信用しないときは、 !!obj と等価にしたいなら括弧を使い、絶対にbooleanを返したいなら条件分岐する、と使い分ける必要がある。

雑記

!! は醜い」「 to_bool を作れ → Rubyで用意して」という意見も見かけた。これに関連して、「 ! しか無いからそうなるのであって対称的な ? を用意すれば改善するのでは」と思い、Rubyの文法をねじ曲げることに挑戦したことがある。


  1. falsenil のみが偽、その他は数字の0でも空文字列でも全て真。 

9
0
0

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
9
0