Rubyのイディオムで、真偽値と見た任意のオブジェクト obj
1 をbooleanである true
または false
に変換する方法として、二重否定 !!obj
がある。
しかしRubyのスタイルガイドはこれを勧めていない。実際にやってみると、以下のコードはRuboCopのチェックにひっかかる。
# 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 anil
check, usenil?
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に変換されているので、あとは true
か false
と比較すればいい。短くなったが更にわかりにくい。
!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回の演算で済む方法。制御構造のほうはオブジェクトをそのまま返しうるのに対し、これらのメソッドは(組み込みクラスの動作を書き換えない限り) true
か false
を返す。前節の方法を配列長1ということに注目して最適化したともとれる。
true & obj
false | obj
nil | obj
false ^ obj
nil ^ obj
- class TrueClass - Rubyリファレンスマニュアル
- class FalseClass - Rubyリファレンスマニュアル
- class NilClass - Rubyリファレンスマニュアル
括弧で区切る
試していたら、これはOKだった。構文解析で !
が連続していないことになっているのだろうか?
!(!obj)
知見
RuboCopの監視をすり抜けられるboolean変換には、以下のような方法が見つかった。
-
!(!obj)
と括弧を使うだけでごまかせる(意外!) -
nil | obj
など論理演算子メソッドでも短く書けて、地味に演算回数も少なくなる-
[obj].any?
などEnumerableのメソッドもかなり短いが、論理演算子メソッドには劣る
-
-
obj ? true : false
などと普通に条件分岐するのが一番わかりやすく、メソッド書き換えの影響も一切受けない
細かいことを言うと、これらはメソッド書き換えを考慮すると等価ではない。外部のコードを信用しないときは、 !!obj
と等価にしたいなら括弧を使い、絶対にbooleanを返したいなら条件分岐する、と使い分ける必要がある。
雑記
「 !!
は醜い」「 to_bool
を作れ → Rubyで用意して」という意見も見かけた。これに関連して、「 !
しか無いからそうなるのであって対称的な ?
を用意すれば改善するのでは」と思い、Rubyの文法をねじ曲げることに挑戦したことがある。
-
false
とnil
のみが偽、その他は数字の0でも空文字列でも全て真。 ↩