Railsを使っていたら、知らない人はいないpresent?
メソッド!!
今回、たまたまRailsのコードを見る機会があり、ついでにpresent?の実装も確認したところ想像以上に作り込まれていたのでまとめてみました。
present?とは
present?についてRailsガイドから抜粋します。
present?メソッドは!blank?メソッドと同等です。
とのことなので、blank?の説明を抜粋します。
Railsアプリケーションでは以下の値を空白(blank)とみなします。
・ nilとfalse
・ ホワイトスペース(whitespace)だけで構成された文字列
・ 空配列と空ハッシュ
・ その他、empty?メソッドに応答するオブジェクトはすべて空(empty)として扱われます。
present?はblank?を反転した結果ということなので、以降ではblank?について考察していきたいと思います。
blank?の実装を推測してみる
実際にコードを見る前に、blank?の実装を推測してみます。
blank?とRubyの偽判定との比較
blank?の実装を推測するため、まずはblank?がtrueと判定する条件とRubyが偽と判定する条件を比較します。
・ nilとfalse
こちらはRubyでも偽と判定されます。
irbで確認。!!
をつけて真偽値に変換しています。
irb(main):002:0> !!nil
=> false
irb(main):004:0> !!false
=> false
・ ホワイトスペース(whitespace)だけで構成された文字列
こちらはRubyでは真と判定されます。
irb(main):005:0> !!' '
=> true
・ 空配列と空ハッシュ
・ その他、empty?メソッドに応答するオブジェクトはすべて空(empty)として扱われます。
Rubyでは配列とハッシュは空でも真と判定されます。
irb(main):001:0> !![]
=> true
irb(main):002:0> !!{}
=> true
上記の挙動より、blank?では「ホワイスペースのみの文字列を偽と判断する」と「empty?メソッドに応答するオブジェクトはemtpry?メソッドで真偽を判定する」の2点が作り込まれいると判断できます。
Railsのコードを見る
では実際にコードを見てみます。
この記事では執筆時点の最新バージョンv7.0.2.2
のコードを参照します。
present?
まずはpresent?の実装を確認します。[GitHub]
class Object
def present?
!blank?
end
end
Railsガイドに記載されていた通り!blank?
となっています。
Rubyでは全てのクラスがObjectクラスを継承しているので、Objectクラスに追加することで全てのオブジェクトがpresent?を使えるようにしているようです。
blank?
次にblank?の実装を確認します。[GitHub]
class Object
def blank?
respond_to?(:empty?) ? !!empty? : !self
end
end
こちらもObjectクラスを拡張しています。
挙動の推測に記載した「empty?メソッドに応答するオブジェクトはempty?メソッドで真偽を判定する」が実装されていることがわかります。
ただ、この実装だけだと「ホワイトスペース(whitespace)だけで構成された文字列」がfalseになってしまいますね。
さらにblank?メソッドを探すと、下記が見つかりました。[GitHub]
class String
BLANK_RE = /\A[[:space:]]*\z/
ENCODED_BLANKS = Concurrent::Map.new do |h, enc|
h[enc] = Regexp.new(BLANK_RE.source.encode(enc), BLANK_RE.options | Regexp::FIXEDENCODING)
end
# A string is blank if it's empty or contains whitespaces only:
#
# ''.blank? # => true
# ' '.blank? # => true
# "\t\n\r".blank? # => true
# ' blah '.blank? # => false
#
# Unicode whitespace is supported:
#
# "\u00a0".blank? # => true
#
# @return [true, false]
def blank?
# The regexp that matches blank strings is expensive. For the case of empty
# strings we can speed up this method (~3.5x) with an empty? call. The
# penalty for the rest of strings is marginal.
empty? ||
begin
BLANK_RE.match?(self)
rescue Encoding::CompatibilityError
ENCODED_BLANKS[self.encoding].match?(self)
end
end
end
Stringクラスに個別にblank?メソッドを追加しています。
こちらは正規表現を使ってスペースのみの文字列はfalseと判定しているようです。
これでRailsガイドに記載されている通りの挙動をしそうですね!
実装の確認は終わり〜、、、と思ったら、blank?/present?の実装はまだまだ先がありました。
さらにその先へ
ここまでの調査でblank?やpresent?が想定通りに動作するための実装はできているのですが、Railsのコードを眺めていたらさらにその先のコードの見つけることができました。
activesupportlib/active_support/core_ext/object/blank.rbを覗いてみます。
全てのコードを載せると大量になってしまうので抜粋しています。
class NilClass
def blank?
true
end
end
class FalseClass
def blank?
true
end
end
class TrueClass
def blank?
false
end
end
class Array
alias_method :blank?, :empty?
end
class Hash
alias_method :blank?, :empty?
end
class Numeric # :nodoc:
def blank?
false
end
end
class Time # :nodoc:
def blank?
false
end
end
ご覧の通り、クラスによってはblank?メソッドを再定義しています。
例えば、NilClassやFalseClassのblank?はtrueが確定しているので、trueを返すだけの単純なメソッドで上書きしています。
逆にNumericやTimeなどfalseが確定しているものは、falseを返すだけの単純なメソッドで上書きしています。
また、empty?メソッドがあることがわかっているクラスはblank?をempty?のエイリアスにしています。
blank?を再定義することでどれくらい差があるのか確認するために、Timeクラスのblank?とObjectクラスのblank?の処理を100万回判定する時間を測定してみました。
そもそも重たい処理ではないのでObjectクラスのblank?の処理でも高速ですが、再定義することで倍くらい速くなっています!!
自分が作っていたらこんな作り込み絶対しないだろうなーと思いつつ、Railsの速度へのこだわりを感じました!!!!
irb(main):001:0> require 'benchmark'
irb(main):002:0> now = Time.now
irb(main):003:0> now.class
=> Time
# blank?で判定
irb(main):004:1* result = Benchmark.realtime do
irb(main):005:1* 1000000.times { now.blank? }
irb(main):006:0> end
=> 0.056137200008379295
# Objectクラスのblank?の処理で判定
irb(main):007:1* result = Benchmark.realtime do
irb(main):008:1* 1000000.times { now.respond_to?(:empty?) ? !!empty? : !now }
irb(main):009:0> end
=> 0.10277090000454336
ついでにblank?も使わず!self
のみの判定も測定してみましたが、わずかにblank?
より高速です。
この記事の主題からずれますが、!self
で判定可能な場合は!self
で判定した方が良さそうです。
# !selfで判定
irb(main):010:1* result = Benchmark.realtime do
irb(main):011:1* 1000000.times { !now }
irb(main):012:0> end
=> 0.04264810000313446
上記の他にもblank?が定義されているクラスはいくつかありました。
記事には載せませんがdef blank?
などでgrepすると簡単に見つかります。
また、present?を再定義している箇所もありました。
その1つがActiveRecordです。[GitHub]
def present? # :nodoc:
true
end
def blank? # :nodoc:
false
end
上記のようにActiveRecordではblank?だけでなくpresent?も定義しています。
このメソッドが追加されたコミットやissueに詳細なコメントが記載されていました。
元々は個別でパッチを当てていたものをRailsに取り込んだようです。
自分だったら絶対に気にしないし、むしろ重複ロジック(なくても動くロジック)を拒んで実装しないけど、数ミリ秒を捻出しようとするパフォーマンスへの強いこだわりを垣間見ることができました!!
おまけ
ActiveRecordの説明で参照したissueにとても重要なことが書かれていたので転記します。
Code that calls present or blank in an active record instance is an anti-pattern, to not say wrong. Making it faster is to legitimate that anti-pattern.
意訳すると『ActiveRecordでは、そもそもpresent?やblank?を使うことがアンチパターンです。それを高速化するということはアンチパターンを正当化することになります。』とのことです。
記事の途中でも『この記事の主題からずれますが、!self
で判定可能な場合は!self
で判定した方が良さそうです。』と記載しましたが、基本的にpresent?やblank?を使わなくてすむ時は使わない方がわずかに高速です(メソッド呼び出しが減るからかな?)。
そのため、present?やblank?はパフォーマンスを考慮された作りになっていますが、利用者である我々も極力Rubyの真偽判定のみで判定するように心がけると良さそうです!
(今思えば、Rails使い始めた当初の現場ではActiveRecordにpresent?やblank?使ってたらレビューで指摘されたので、良い現場でRailsを覚えられたんだなーと思いました)