Tech Racho エンジニアの「?」を「!」に。
  • 開発

[Rails5] Active Support Core ExtensionsのStringクラス(1)String#blank?

こんにちは、hachi8833です。

いよいよActive Support世界を探索してみます。最初のシリーズは、多くの開発者が直接使うことの多そうなCore Extensions(コア拡張機能)のなかから、さらによく使われていそうなStringを選びました。

第1回は、その中からさらにString#blank?にスポットを当ててみます。

始める前に

特記ない限り、Rails 5の執筆時点のアクティブなブランチを対象とします。Rubyについても執筆時点の最新リリースを対象とします。

  • Rails: 5.0.0.1(core_ext)
  • Ruby: 2.3.1(記事作成中に入れ違いで2.3.2がリリースされましたね)

本シリーズではライブラリを網羅的に紹介することはしません。そうした目的にはapi.rubyonrails.orgapidock.com/railsなどをご覧ください。

本シリーズでは、Core Extensionsは原則として英ママまたはcore_extと表記することにします。Railsガイドで「コア拡張機能」という訳語を当てたのは私なので恐縮ですが、あまりイケてなかったと思います。

activesupport/lib/active_support/core_ext

github.com/rails/railsの5.0.0.1 ブランチを開き、activesupport/lib/active_support/core_extまで降りてみましょう。ドラクエのダンジョンBGMでもどうぞ。

なお、記事作成時点のmasterブランチで最新のcore_extディレクトリを見ると、1年以上更新されていないファイルは1つ(name_error.rb)しかありませんが、5.0.0.1では同じぐらい更新されていないファイルが他にもあります。

Railsのmasterブランチは安定版ではなく、逆にmasterブランチでがんがん開発が進められているので、普段git-flowを使っていると「master=本番」と反射的に考えてしまうので、少々とまどいますね。

active_support/core_ext/string

ではもう1階層降りて、active_support/core_ext/stringを開いてみましょう。.rbファイルが13個あります。

おや、上の真っ赤なString一覧には.rbファイルが15個記載されていました。数が合いません。

よく見ると、Stringの一覧のうち、blank.rbとjson.rbだけはstring/ではなくobject/の下に置かれています。

何か理由があるのでしょうか。これはもうobject/を見てみるしかないでしょう。とりあえずblank.rbを開けてみます。

active_support/core_ext/object/blank.rb

おお、中はこうなっているのですね。なお短縮のため、途中のコードやコメントは省略しました。メソッド間の改行も詰めていますのでご了承ください。

## blank.rb
require "active_support/core_ext/regexp"
class Object
  def blank?
    respond_to?(:empty?) ? !!empty? : !self
  end
  def present?
    !blank?
  end
  def presence
    self if present?
  end
end

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 String
  BLANK_RE = /\A[[:space:]]*\z/
  def blank?
    empty? || BLANK_RE.match?(self)
  end
end
## 以下略

blank.rb全般

Objectクラスばかりでなく、さまざまなクラスに#blank?が仕込まれています。Active Support全般に言えることですが、1つ1つがとても短いコードが多いのが特徴です。これなら読み進められそうです。

ArrayクラスとHashクラス

  • ArrayクラスやHashクラスでは#blank?#empty?の別名としています。

そういえばRubyには以前から#empty?というメソッドがありました。

Rubyではnilfalseのみが偽(false)になります。一方、Webアプリケーションでは空文字列や空の検索結果なども例外的な処理対象としたい場面がよくあります。そのため、Railsには広い意味で「空(から)」と認定したいものを偽と判定するメソッドが用意されています。
Rails 3レシピブック』p346より、若干改変のうえ引用

つまり、Rubyの#empty?では意味が狭すぎるので、Rails向けに拡張したものが#blank?であるいうことです。ArrayクラスやHashクラスでは現状の動作をそのまま流用できるので、エイリアスだけを設定したのですね。

Stringクラス

本題のString#blank?です。

Stringクラスでは、空文字列もnilと認定したいので、次のようなコードになっています。

require "active_support/core_ext/regexp"
#略
class String
  BLANK_RE = /\A[[:space:]]*\z/
  def blank?
    empty? || BLANK_RE.match?(self)
  end
end
  • 正規表現を使うためにactive_support/core_ext/regexpをrequireしています。Ruby標準の正規表現そのままではなく、Active Supportで拡張した正規表現を使っているのですね。

  • /\A[[:space:]]*\z/という正規表現で、空白文字の連続にマッチさせています。

なお、Rubyの正規表現エンジンであるOnigmoでは、:space:について以下のように定義しています。空白文字といっても実はさまざまな種類があるので、個別の文字をいちいち指定するよりもこうしたメタ的な指定方法がよさそうです。

  • Unicode以外の場合: \t, \n, \v, \f, \r, \x20
  • Unicodeの場合: Space_Separator | Line_Separator | Paragraph_Separator | 0009 | 000A | 000B | 000C | 000D | 0085
  • empty? ||BLANK_RE.match?(self)で、nilか空白文字の連続であればtrueを返し、それ以外の場合はfalseを返します。

Railsコンソール(bundle exec rails c)で実行結果を確認してみました。

復習: Rubyはオープンクラス

Rubyはオープンクラス方式を採用しているので、Rubyスクリプトで読み込まれているクラスにその場で動的にメソッドを追加・変更できます。これは、相手がRubyの標準ライブラリであってもまったく同じようにできます。

また、Rubyスクリプトが終了すれば動的な変更も元に戻るので、他のRubyスクリプトに悪影響を与えずに済みます。

もちろん、ObjectやStringなどの基本的なクラスに手を加えれば、実行中のRubyスクリプト内で広範囲に影響を与えます。これを「強力」と呼ぶか「危険」と呼ぶかは使う人次第だと思います。

Railsを含むRubyプログラムで広く使われている「メタプログラミング」と呼ばれる技法では、このオープンクラスの特性が多用されています。

追伸: String#blank?がobject/blank.rbに置かれているのはなぜか

オープンクラスからString#blank?に話を戻します。

blank.rbを見ると、#blank?メソッドはObjectクラス、NilClassFalseClassTrueClassの三兄弟クラス、ArrayクラスにHashクラス、そしてStringクラスに追加されています。

クラスを使う側からすればString#blank?などと呼び出せればよいので、クラスのファイルをどのように配置するかは主にメンテナ側の都合で決まることが多いようです(あくまで私の印象ですが)。

一見すれば、core_ext/string/blank.rbというファイルを作ってそこでString#blank?を定義してもよさそうなものですが、他のクラスのblank?もまとめてobject/blank.rbファイルに置く方がcore_extメンテナにとって見通しがよいと思われたのかもしれません。

その一方、ActiveSupport::TimeWithZone#blank?ActiveRecord::Relation#blankのように、別のクラスファイルにも#blank?があったりします。これらが別になっている理由まではわかりませんが、歴史的経緯などが影響しているのではないかと推測しています。

こうしたクラス配置を原理主義的にびっちり再編成しようとしても労力の割に大したメリットがなかったりするので、メンテナにとって支障がなければ今後もこのままでいくのかもしれません。

後日談: core_ext/objectはメソッド名がファイル名になっていることが多い

BPS Webチーム部長のmorimorihogeさんとActive Supportを眺めていて、「core_ext/objectにはメソッド名がそのままファイル名になっているものが目につくね」という指摘がありました。

すべてではありませんが、言われてみると今回のblank.rbにはblank?メソッドがありますし、deep_dub.rbにはdeep_dubメソッドがあり、try.rbにはtryメソッドがまとまっている、という具合になっています。ディレクトリやメソッドをそういう目安で整理しているのかもしれません。

「core_ext/objectと比べてみると、たとえばcore_ext/stringはfilters.rbfiltersというメソッドがあるわけではないね。はっきりした根拠があるわけではないけれど、core_ext/stringは、メソッド名ではなく機能をファイル名にする方針なのかもしれない」とも。

多数のコミッターが日夜メンテを繰り返しているActive Supportには、そういった秩序があっても不思議ではありません。今度から、そういう視点にも気をつけてコードを見るようにします。

関連記事


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。