Kōhei Yamamoto

適切な場面でRailsのActive Supportのblank?/present?を使う

先に結論

RailsのActive Supportのコア拡張が提供するblank?present?は、次のようなケースで使っていきたい。

params[:maybe_absent_or_empty].blank?
ENV["MAYBE_ABSENT_OR_EMPTY"].present?

配列やPORO、Active Recordのオブジェクトなどに対して、値がnilかどうかや、要素数がゼロかどうかを確かめるだけならblank?/present?を使わなくてもよい。オブジェクトの真偽をそのまま評価する、&.を使う、またはnil?empty?などを適切に使うと読みやすい。

if maybe_nil_obj
# ...
end
maybe_nil_obj&.do_something
if maybe_nil_or_empty_array.nil?
# ...
end
unless maybe_empty_array.empty?
# ...
end

前提知識

参考資料:

blank?/present?はActive Supportが提供するコア拡張という機能の一部。

blank?はあるオブジェクトが「空っぽい」ならtrueを、そうでないならfalseを返す。blank?で空とみなすのは、nilfalse・空文字・[]{}、そのほかempty?に反応してtrueを返すものなどがある。

nil.blank? #=> true
"".blank? #=> true
[].blank? #=> true
Set[].blank? #=> true

present?blank?の逆になっている

blank?で特有の仕様を持つクラスもある。String#blank?POSIX文字クラス/[[:space:]]/に一致する空白文字を考慮する。つまり、空白文字だけからなる文字列"  \t"(半角空白、全角空白、タブ文字)に対して

"  \t".blank? #=> true

となる。

問題

blank?/present?は、空っぽいものを楽に判別できるし、ある種「雄弁な」メソッド名なのでコードの表現力を高めてくれるようにも感じられる。

一方、とりあえずで使ってしまいやすいのも事実で、その結果、どのような値が来ることを想定してblank?/present?を使っているのかコードを読むときに考える必要が出てきてしまう。また、blank?が空と見なす値の種類が多いので、条件式で意図しない値が来たときに誤った判定になってしまう可能性もある。

最近だと、rubocop-railsのRails/Presence copでは次のような変換が実行される(presencepresent?に基づいたメソッド1

# bad
foo.do_something(bar) if foo.present?
# good
foo.presence&.do_something(bar)

badのスタイルで、なんらかのオブジェクトが入っているかもしれないfooに対してpresent?が使われているとき、goodのスタイルにコードを書き換えても、必ずしも可読性が上がるとは言いがたい。これはpresent?の使いかたを変えれば問題ごとなくせるのではないかと考えている。

この話題はWeb上で何年にもわたって語られており、意見も分かれやすい。

当記事は(あえて?)抑制的な意見の側に立っているといえる。

APIの意図を考える

Active Supportがblank?/present?というAPIを用意している意図の1つは、Guidesの記述からも明らかなように、外部環境から入力される値に対して空かどうか判定するためといえる。ここで、環境とはWeb (HTTP)やOSなどを指す。

環境からの値はHashライクなキーと値のペアのことが多い。値は文字列、もしくは配列やHashとして渡される。例としては、HTTPリクエストを入力とするときのヘッダー・クエリパラメータ・ボディ・Cookieなど、またOSから入力される環境変数やコマンドライン引数の値がある。

アプリケーションは環境から入力される値をひとまずはそのまま受け取るしかない。何かの値が「空」のとき、そのキー自体がなかったり、キーはあるが値がnilだったり、値は入っているが空っぽい文字列だったりする。

blank?/present?は、このようなデータに対して空っぽいかどうかを判定できるという点で価値がある。

Active Recordバリデーションではどうか

Active Record (AR)のバリデーションにvalidate_presence_of/validate_absence_ofというものがあり、その内部ではblank?/present?を使っている2

これは理にかなっている。なぜなら、Railsでの標準的なコードの書きかたは、環境から入力されたparamsなどの信頼できない値をARのオブジェクトにセットして、その値に対してバリデーションを実行するというものだからだ。

RailsではARのモデルがフォームオブジェクトとドメインモデルの性質を合わせ持つ。つまり、環境からの入力のチェックと、モデルの不変条件を守るバリデーションを同じところに書くという割り切りをしている。

そういった状況で、入力が広い意味で空っぽいかどうかを判断しつつ不変条件を表現するために、該当のバリデーションではblank?/present?を内部的に使う必要がある。このバリデーションを使うときに空っぽいものは弾きつつnilだけは許したいなら、allow_nil: trueを使うことになる。

Rubyらしく書く

結局blank?/present?を使わずにどう書くかについて。これはふつうのRubyっぽく書く、という素朴な話になってしまう。

たとえば、アプリケーションの内部で生成したPOROやActive Recordのオブジェクトが入っているかもしれない変数を調べるときは、オブジェクトが存在するかnilかのどちらかと想定する。また、文字列や配列についても、アプリケーション内部で生成するなら、必ず空配列で初期化するなど、変数が取る値は制御できる。

この場合はblank?/present?を使わなくても、nil?empty?has_key?で値があるかどうか判定するコードのほうが自然といえる。また、条件式を作る場合は、Rubyではnil/false以外はすべてtruthyであることを使うのが自然といえる。

たとえば、上述のrspec-railsの例で挙げたコードは次のように書き換えられる。ここで、fooはアプリケーション内で生成したある程度複雑なオブジェクトが入っているとする。

# Rails/Presenceで書き換え対象になるコード
foo.do_something(bar) if foo.present?
foo.do_something(bar) if foo
foo&.do_something(bar)

falsenil以外はすべてtruthyなので、1番のように書けば十分意図を表現できる。また、fooがfalse以外の一般的なオブジェクトか、もしくはnilが来ることを想定しているのなら、2番のコードのようにsafe navigation operatorを使えば、より本来の意図を表現できる。

また、blank?/present?は空と見なす値の種類が多いので、empty?などで条件式を書けるのであれば、それをそのまま書いたほうが読み手に意図が伝わりやすい。また、それでNoMethodErrorが起きるのであれば、想定外のオブジェクトが来るというバグが潜んでいるので、それに早めに気づけるというのもよい点といえる。

unless maybe_empty_array.empty?
# ...
end

脚注

  1. v2.34.0以降

  2. たとえば、ActiveRecord::Validations::PresenceValidatorから委譲されるActiveModel::Validations::PresenceValidator#validate_eachblank?を使っている