LoginSignup
7
6

More than 1 year has passed since last update.

Railsのpresent?の実装を覗いてみたら想像以上に作り込まれていた話(でもpresent?使わなくても判定可能なら使わない方が良いよ)

Last updated at Posted at 2022-03-07

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]

activesupport/lib/active_support/core_ext/object/blank.rb
class Object
  def present?
    !blank?
  end
end

Railsガイドに記載されていた通り!blank?となっています。
Rubyでは全てのクラスがObjectクラスを継承しているので、Objectクラスに追加することで全てのオブジェクトがpresent?を使えるようにしているようです。

blank?

次にblank?の実装を確認します。[GitHub]

activesupport/lib/active_support/core_ext/object/blank.rb
class Object
  def blank?
    respond_to?(:empty?) ? !!empty? : !self
  end
end

こちらもObjectクラスを拡張しています。
挙動の推測に記載した「empty?メソッドに応答するオブジェクトはempty?メソッドで真偽を判定する」が実装されていることがわかります。
ただ、この実装だけだと「ホワイトスペース(whitespace)だけで構成された文字列」がfalseになってしまいますね。

さらにblank?メソッドを探すと、下記が見つかりました。[GitHub]

activesupport/lib/active_support/core_ext/object/blank.rb
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を覗いてみます。
全てのコードを載せると大量になってしまうので抜粋しています。

activesupport/lib/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]

activerecord/lib/active_record/core.rb#L622
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を覚えられたんだなーと思いました)

7
6
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6