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

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

[^tf]: `false` と `nil` のみが偽、その他は数字の0でも空文字列でも全て真。

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

```ruby: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
```

```shell-session
$ 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に変換したいことはある。

* [Style/DoubleNegation should be allowed for return values · Issue #591 · rubocop-hq/ruby-style-guide](https://github.com/rubocop-hq/ruby-style-guide/issues/591)

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
>
> ```ruby
> # bad
> !!something
>
> # good
> !something.nil?
> ```

例を真似るとこうなる。

```ruby
  !obj.nil?
```

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

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

## 捕まる例
### スペースを入れる
`!!` と連続しているのを捉えているなら、間を空ければごまかせそう。

```ruby
  ! !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#!` を使う
単項演算子だからダメなだけで、メソッドとして呼び出せば見逃がされるかもしれない。

* [instance method BasicObject#! - Rubyリファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/method/BasicObject/i/=21.html)

```ruby
  !obj.!
```

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

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

### `not` を組み合わせる
`!` と優先順位が異なる演算子なので、連続しているとは判定されないはず。

* [演算子式 - Rubyリファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/doc/spec=2foperator.html)

```ruby
  not !obj
```

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

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

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

```ruby
  !obj.__send__(:!)
```

* [instance method BasicObject#\_\_send\_\_ - Rubyリファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/method/BasicObject/i/__send__.html)

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

```ruby
  if obj
    true
  else
    false
  end
```

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

```ruby
  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
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
```

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

```ruby
  obj ? true : false
```

### 論理演算子（制御構造）を使う
`&&` や `||` の短絡評価の性質を利用する。残念ながら三項演算子より長いしわかりにくい。

```ruby
  obj && true || false
```

### 論理否定を1回のみにする
`!` 1回で既にbooleanに変換されているので、あとは `true` か `false` と比較すればいい。短くなったが更にわかりにくい。

```ruby
  !obj == false
  !obj != true
```

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

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

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

```ruby
  ![obj].none?
```

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

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

```ruby
  ![obj].none?.itself
```

* [module Enumerable - Rubyリファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/class/Enumerable.html)
* [instance method Object#itself - Rubyリファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/method/Object/i/itself.html)

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

```ruby
  true & obj

  false | obj
  nil | obj

  false ^ obj
  nil ^ obj
```

* [class TrueClass - Rubyリファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/class/TrueClass.html)
* [class FalseClass - Rubyリファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/class/FalseClass.html)
* [class NilClass - Rubyリファレンスマニュアル](https://docs.ruby-lang.org/ja/latest/class/NilClass.html)

### 括弧で区切る
試していたら、これはOKだった。構文解析で `!` が連続していないことになっているのだろうか？

```ruby
  !(!obj)
```

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

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

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

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

* [`BasicObject#?` は作れるか - Qiita](/HMMNRST/items/75811cf547dad3c0c681)