active support

こんばんは、久保田です。

今回はRailsを丸裸にする第二弾、「active support」について取り上げたいと思います。

みなさんRailsを使っているならば、素のRubyでは使えない

obj.try(:to_s)

obj.present?

などはたくさん使っているでしょう。

これらのメソッドは大抵「active support」というgemによって追加されています。

README.rdocを見てみると

Active Support is a collection of utility classes and standard library extensions that were found useful for the Rails framework. These additions reside in this package so they can be loaded as needed in Ruby projects outside of Rails.

超ざっくり訳

「Active SupportはRailsフレームワークのための便利なクラスとか標準クラスを拡張しているんや。
これらは必要ならばRails以外のRubyでも使えるで。」

みたいなことを言っていますね。

ということで今回は、activesupportがどのようにutilityクラスを定義しているのか、そしてどのようにクラスを拡張しているのかをざっくり見て行こうかなと思います。

今回見るのは、
activesupport 5.0.0beta3
です。

いきなり少し寄り道しますが、rails系のgemは、version管理も少し特徴があるみたいです。
今回version確認の際に気がつきました。

gemにはversionを管理するためgemを作るときに提供されるversion.rbというファイルにバージョンを書いて置きgemspecというgemのメタデータを管理するファイルから呼び出すのですが、

rails系のgemはversion.rbからさらにgem_version.rbにversionを管理するmoduleを定義し、それをActiveSupport.gem_versionというメソッドから呼び出しているみたいです。かなり細分化してますね。。。
ルールでも決まっているのでしょうか。

# version.rb

require_relative 'gem_version'

module ActiveSupport
# Returns the version of the currently loaded ActiveSupport as a <tt>Gem::Version</tt>
def self.version
gem_version
end
end
# gem_version.rb

module ActiveSupport
# Returns the version of the currently loaded Active Support as a <tt>Gem::Version</tt>.
def self.gem_version
Gem::Version.new VERSION::STRING
end

module VERSION
MAJOR = 5
MINOR = 0
TINY = 0
PRE = "beta3"

STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
end

すごいですね。意図はよくわからないですが、なんかすごいですね。笑
今度作るときは真似しようと思います。笑

さて、話をactivesupportに戻します。

まずはlib以下にあるactive_support.rbです。

まずはmoduleでnamespaceを作っていますね。こうすることによって他のクラスやモジュールが安全に作られます。

require "active_support/dependencies/autoload"

...

module ActiveSupport
extend ActiveSupport::Autoload

autoload :Concern
autoload :Dependencies
autoload :DescendantsTracker
autoload :FileUpdateChecker
autoload :EventedFileUpdateChecker
autoload :LogSubscriber
autoload :Notifications

eager_autoload do
autoload :BacktraceCleaner
autoload :ProxyObject

....

end

いやぁ、めちゃくちゃautoloadしていますね。。。
このautoloadですが、”active_support/dependencies/autoload”をrequireすることで得られるクラス ActiveSupport::Autoloadをextendすることによって得られる特異メソッドです。
もともとのModule.autoloadをオーバーライドしているようですね。
拡張することで、パスを渡さなくてもRailsのクラス名の規約(bar.rb => class Bar)に則ってファイルを探してくれるらしいです。

def autoload(const_name, path = @_at_path)
unless path
full = [name, @_under_path, const_name.to_s].compact.join("::") # ここでフルパス作成。Hoge::Fuga::Fooみたいになるっぽい
path = Inflector.underscore(full) # activesupport/inflector/methods.rb で定義されているunderscoreを使って、CamelCaseをpathに変換
end

if @_eager_autoload # eager_autoload内でautoloadが呼ばれていたら、@_eager_autoloadがtrueなので、ここを通る
@_autoloads[const_name] = path #@_autoloadsにconst_nameとpathを対応させる。eager_load!が呼ばれたらすべてrequireする。
end

super const_name, path #もともとのautoloadに渡す。
end

さらにはeager_autoloadというメソッドも提供しています。これはブロックを渡しその中でautoloadをしたものをeager_autoload!で一気に読み込むらしいです。

# ex
eager_autoload do
auto_load :Hoge
auto_load :Foo
end

eager_autoload! # ここでhoge.rbのHoge, foo.rbのFooを一気に読み込み。

こういう風にしてたくさんのutilityなクラスを効率よく読み込んでいるみたいですね。

では標準クラスの拡張はというと、
どうやらactive_support/core_ext/ 以下にたくさんそれらしきものがあります。

そしてどのように呼び出し、拡張しているのだろう、と思ったのですが、わからなかったので調べたところ、公式サイトにこのように書かれていました。

Ruby on Railsアプリケーションでは、基本的にすべてのActive
Supportを読み込みます。例外はconfig.active_support.bareをtrueに設定した場合です。

Railsが勝手に読むらしいですね。どのように呼んでいるのかまではわかりませんでした。

そしてcore_ext以下を見てみると、ものすごく細分化されていました。
それぞれ拡張するクラス毎に別れており、オープンクラスをしまくっているようです。

例としてactive_support/core_ext/string/zones.rbを見てみます。
これは文字列を指定したタイムゾーンにパースしてくれるメソッドです。

# active_support/core_ext/string/zones.rb

...

class String
# Converts String to a TimeWithZone in the current zone if Time.zone or Time.zone_default
# is set, otherwise converts String to a Time via String#to_time
def in_time_zone(zone = ::Time.zone)
if zone
::Time.find_zone!(zone).parse(self)
else
to_time
end
end
end

# 使用例
"2016/4/1".in_time_zone("EST")
# => Fri, 01 Apr 2016 00:00:00 EST -05:00

このメソッド1つのためにファイルが作られています。

こんな風にユーティリティクラスはnamespaceを確保し、autoloadし、
core_extはRailsに読ませることで拡張しているようですね。

少し長くなってしまいましたが、まだまだ奥が深そうなので、次回は普段使っているメソッドがどのように定義されているかを見てみたいと思います!

ではまた次回!!


ちなみにRails外からactivesupportを使うときは、

require "active_support/all"

または、core_extだけが必要なときは、

require "active_support”
require "active_support/core_ext"

と書けば使用可能です。
“active_support/core_ext”は以下のように、core_ext以下を一気にrequireしています。