【Rails】「nilじゃない」と「present?」は同じ？ 〜わかりにくいコードに立ち向かう〜

こんにちは
業務でリファクタをしていて、タイトルの件少し気になったのでまとめてみます

少しややこしいですが、頭の体操だと思ってお読みください
（混乱しないように、わかりやすくまとめたつもりではあります。一応。。）

否定表現ってどうしてわかりにくいんでしょうね。ただ意味が逆になるだけなのに...

`※ 2021/8/29に修正しています（公式ドキュメントの参照先）`

# 本題
タイトルの通り`nilじゃない`という条件は`present?`と代替可能か？というテーマです

```ruby
if !hoge.nil? # => hoge.present?でもいいのでは？
  # ...
end
```

上記の例だと、「`nil`の場合に早期リターンすればいいだけじゃん」で片付くかもしれません（それか`unless`）

ですが、条件が複雑になってくると`present?`にしたい欲が込み上げてきて吐きそうになります🤢
（以下は吐きたくなる例）

```ruby
if !hoge.nil? && fuga.present? # .｡oO hoge.present?にしちゃだめ・・？
  # ...
elsif fuga.status == 'canceled' && piyo.status == 'active'
  # ...
else
  # ...
end
```

※ ここで「`status_canceled?`のメソッドを用意しておくべきだろ」みたいなコメントはポケットの中にでもしまっておいてください（本記事の趣旨とは関係ない話なので）

## TL;DR
以下の条件を満たす`hoge`が想定される場合は、置き換えてはいけません

```ruby
!hoge.nil?
# => true

hoge.present?
# => false
```

💡**つまり、レシーバが以下のオブジェクトとなりうる場合は置き換えできません**

* `''`（空文字）
* `' '`（スペースのみの文字列）
* `{}`（空のハッシュ）
* `[]`（空の配列）
* `false`

それか、これらのオブジェクトが来ても無視してOKという仕様であれば置き換えても良さそうです

以下「なぜそうなる？」という話が続きます

## 調査
こういう調査は公式のドキュメントを読むに限ります
もし間違っていても「ドキュメントが悪い」ということで刑が軽くなるからです
（内部のソースコードまでは読んでないので、ガチ勢の方がいらっしゃいましたら優しくご指摘ください）

### nil?について
まずは`nil?`というメソッドについて、調べてみます

https://docs.ruby-lang.org/ja/latest/method/Object/i/nil=3f.html
> レシーバが nil であれば真を返します。

まあ、当たり前ですが`nil`だけ`true`となります（`nil`以外は`false`）

### present?について
続いて`present?`について


https://api.rubyonrails.org/classes/Object.html#method-i-present-3F
> An object is present if it's not blank.

ではblank?って何？・・↓

https://api.rubyonrails.org/classes/Object.html#method-i-blank-3F
> An object is blank if it's false, empty, or a whitespace string. For example, nil, '', ' ', [], {}, and false are all blank.

`blank?`はいろんなクラス（StringとかActiveRecord::RelationとかNilとか..）に対して定義されています

`false`や`empty`や`空白の文字列`はblankという判定になります

ではempty?って何だろう？・・↓
empty?はRuby標準のメソッドです

https://docs.ruby-lang.org/ja/latest/method/Array/i/empty=3f.html

https://docs.ruby-lang.org/ja/latest/method/Hash/i/empty=3f.html

Railsでは`ActionController::Parameters`や`ActiveRecord::Relation`などにも用意されています（他のクラスにもあるので詳しくはドキュメント参照）

https://api.rubyonrails.org/classes/ActionController/Parameters.html#method-i-empty-3F

https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-empty-3F

例えば`ActionController::Parameters`であればキー/バリューがない場合だったり、`ActiveRecord::Relation`であればレコードがない場合にはemptyの判定になります
（直感的にはそうですね）


厳密な表現ではないですが、直感的には`present?`が`true`を返すオブジェクトは、[`空の文字列`, `スペースのみ文字列`, `空のハッシュ`, `空の配列`, `nil`, `false`]の補集合と考えて良さそうです

まあつまり[`空の文字列`, `スペースのみ文字列`, `空のハッシュ`, `空の配列`, `nil`, `false`]に対しては`false`を返すってことです



以下、公式ではないですが日本語での調査です

<details><div>

https://railsdoc.com/page/empty

> !blank? を実行するメソッド。if 変数.present?とunless 変数.blank?は同じ意味 nil, “”, “ “(半角スペースのみ), 空の配列, 空のハッシュのときにfalseを返します Railsで拡張されたメソッドで、Rubyのみでは使えないのでご注意ください

ではblank?って何？・・↓
> nil? + empty? のようなメソッド。nilまたは空のオブジェクトをチェック nil, “”, “ “(半角スペースのみ), 空の配列, 空のハッシュのときにfalseを返します Railsで拡張されたメソッドで、Rubyのみでは使えないのでご注意ください

ではempty?って何？・・↓
> Rubyの標準メソッド。空の文字列や空の配列の場合にtrueを返す。nilに対して呼び出すとNoMethodErrorが発生

</div></details>


## 検証
`present?`はRailsでのみ使えるメソッドなので、Railsのコンソールで検証してみます

```ruby
''.present?
# => false

' '.present?
# => false

{}.present?
# => false

[].present?
# => false

nil.present?
# => false

true.present?
# => true

false.present?
# => false
```

若干くどいですが、念の為`nil?`についても試してみます
当然`nil?`は`nil`以外に対しては`false`です

```ruby
''.nil?
# => false

' '.nil?
# => false

{}.nil?
# => false

[].nil?
# => false

nil.nil?
# => true

true.nil?
# => false

false.nil?
# => false
```

当たり前ですが、ドキュメントのとおりの結果となりました

ここで`nil?`に対しても`present?`に対しても`false`を返しているオブジェクトがあるので、`nilじゃない`という判定`present?`と置き換えてはいけないことになります


※ 当然ですが、文脈的に置き換えても問題ないという仕様であれば置き換え可能です（上記のケースをよく考える必要があるので少し疲れます）


# まとめ

まとめると、当然のようですが、`nilじゃない`と`present?`は別物になります
置き換えたいときは例外ケースがないか十分に検討してからのほうが良さそうですね

今回の裏テーマとしては、**「最初から読みやすいコードを書くように心がけよう」**です
読みにくいコードは、`これまで問題を起こさず生き延びてきた`という謎の実績を武器に私たちの前に立ちはだかるのです😈
そして貴重な時間を半永久的に奪っていきますorz

最初から読みやすいコードを書いてくれる人には「ありがとう」と伝えなくてはなりませんね❤️

1bitでもためになったと感じたら、あなたがすべきことはLGTMをポチることだけです
（他の人にも読んでもらいやすくなりますので）

ではでは良いプログラミングライフを〜