Railsのソースコード読んでみる | Active Support constantize編

f:id:sktktk1230:20190921180106p:plain

普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

constantize を今日は読んでみようと思います

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
Railsの日本語ドキュメントを見てみると

説明

引数の文字列で指定した名前で定数を探す


使い方

文字列.constantize()

'Module'.constantize     # => Module
'Test::Unit'.constantize # => Test::Unit

引用:Railsドキュメント:constantize

ソースコードを読んでみる

1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def constantize を探してみます

f:id:sktktk1230:20180905141912p:plain

2. 該当箇所が2箇所ほどあったので、それぞれみてみます

1. activesupport > lib > active_support > core_ext > string > inflections.rb
# frozen_string_literal: true

require "active_support/inflector/methods"
require "active_support/inflector/transliterate"

# String inflections define new methods on the String class to transform names for different purposes.
# For instance, you can figure out the name of a table from the name of a class.
#
#   'ScaleScore'.tableize # => "scale_scores"
#
class String

省略

  # +constantize+ tries to find a declared constant with the name specified
  # in the string. It raises a NameError when the name is not in CamelCase
  # or is not initialized.  See ActiveSupport::Inflector.constantize
  #
  #   'Module'.constantize  # => Module
  #   'Class'.constantize   # => Class
  #   'blargle'.constantize # => NameError: wrong constant name blargle
  def constantize
    ActiveSupport::Inflector.constantize(self)
  end

省略

String クラスをオープンクラスして constantizeメソッドを実装しています

処理は ActiveSupport::Inflector.constantize へ移譲しているので、こちらをみてみます

2. activesupport > lib > active_support > inflector > methods.rb
省略

    # Tries to find a constant with the name specified in the argument string.
    #
    #   constantize('Module')   # => Module
    #   constantize('Foo::Bar') # => Foo::Bar
    #
    # The name is assumed to be the one of a top-level constant, no matter
    # whether it starts with "::" or not. No lexical context is taken into
    # account:
    #
    #   C = 'outside'
    #   module M
    #     C = 'inside'
    #     C                # => 'inside'
    #     constantize('C') # => 'outside', same as ::C
    #   end
    #
    # NameError is raised when the name is not in CamelCase or the constant is
    # unknown.
    def constantize(camel_cased_word)
      names = camel_cased_word.split("::".freeze)

      # Trigger a built-in NameError exception including the ill-formed constant in the message.
      Object.const_get(camel_cased_word) if names.empty?

      # Remove the first blank element in case of '::ClassName' notation.
      names.shift if names.size > 1 && names.first.empty?

      names.inject(Object) do |constant, name|
        if constant == Object
          constant.const_get(name)
        else
          candidate = constant.const_get(name)
          next candidate if constant.const_defined?(name, false)
          next candidate unless Object.const_defined?(name)

          # Go down the ancestors to check if it is owned directly. The check
          # stops when we reach Object or the end of ancestors tree.
          constant = constant.ancestors.inject(constant) do |const, ancestor|
            break const    if ancestor == Object
            break ancestor if ancestor.const_defined?(name, false)
            const
          end

          # owner is in Object, so raise
          constant.const_get(name, false)
        end
      end
    end

省略

freeze で文字列 :: を変更不可のオブジェクトとし、引数camel_cased_word:: でsplitしています

※ splitの仕様はこちら
instance method String#split (Ruby 2.6.0)

次にこちらをみてみます

# Trigger a built-in NameError exception including the ill-formed constant in the message.
Object.const_get(camel_cased_word) if names.empty?

引数をsplitした配列の要素が0のときに実行される処理です
※ Array#empty?の仕様はこちら
instance method Array#empty? (Ruby 2.6.0)

Object.const_get を調べてみます

name で指定される名前の定数の値を取り出します。


Module#const_defined? と違って Object を特別扱いすることはありません。


[PARAM] name:

定数名。String か Symbol で指定します。 完全修飾名を指定しなかった場合はモジュールに定義されている name で指定される名前の定数の値を取り出します。

[PARAM] inherit:

false を指定するとスーパークラスや include したモジュールで 定義された定数は対象にはなりません。

[EXCEPTION] NameError:

定数が定義されていないときに発生します。

module Bar
  BAR = 1
end
class Object
  include Bar
end
# Object では include されたモジュールに定義された定数を見付ける
p Object.const_get(:BAR)   # => 1

class Baz
  include Bar
end
# Object以外でも同様
p Baz.const_get(:BAR)      # => 1
# 定義されていない定数
p Baz.const_get(:NOT_DEFINED) #=> raise NameError
# 第二引数に false を指定すると自分自身に定義された定数から探す
p Baz.const_get(:BAR, false) #=> raise NameError
# 完全修飾名を指定すると include や自分自身へ定義されていない場合でも参照できる
p Class.const_get("Bar::BAR") # => 1

引用:Ruby 2.5.0 リファレンスマニュアル#const_get

nameで指定されている名前の定数を取り出す処理で、完全修飾名であれば、自分へ定義されていない場合でも参照できる処理になります
コメントを読んでみると、引数camel_cased_word が間違った名称だった場合に、NameErrorを発生させる為に使用しているようです

プルリクエストはこちらです
Fix #10932. Treat "" and "::" as invalid on constantize by killthekitten · Pull Request #10943 · rails/rails · GitHub

次にこちらを読みます

# Remove the first blank element in case of '::ClassName' notation.
names.shift if names.size > 1 && names.first.empty?

こちらの処理はさきほどのコミットで追加されています
::ClassNameをsplitするケースだと配列の先頭が空になる為、先頭の要素を取り除く処理を実行しています
f:id:sktktk1230:20180905142049p:plain

※ Array#shiftの仕様はこちら
instance method Array#shift (Ruby 2.6.0)

次にこちらをみていきます

names.inject(Object) do |constant, name|
    if constant == Object
        constant.const_get(name)
    else
        candidate = constant.const_get(name)
        next candidate if constant.const_defined?(name, false)
        next candidate unless Object.const_defined?(name)

        # Go down the ancestors to check if it is owned directly. The check
        # stops when we reach Object or the end of ancestors tree.
        constant = constant.ancestors.inject(constant) do |const, ancestor|
        break const    if ancestor == Object
        break ancestor if ancestor.const_defined?(name, false)
        const
        end

        # owner is in Object, so raise
        constant.const_get(name, false)
    end
end

まず inject の仕様を調べます

リストのたたみこみ演算を行います。


最初に初期値 init と self の最初の要素を引数にブロックを実行します。 2 回目以降のループでは、前のブロックの実行結果と self の次の要素を引数に順次ブロックを実行します。 そうして最後の要素まで繰り返し、最後のブロックの実行結果を返します。


要素が存在しない場合は init を返します。


初期値 init を省略した場合は、 最初に先頭の要素と 2 番目の要素をブロックに渡します。 また要素が 1 つしかなければブロックを実行せずに最初の要素を返します。 要素がなければブロックを実行せずに nil を返します。


[PARAM] init:

最初の result の値です。任意のオブジェクトが渡せます。

[PARAM] sym:

ブロックの代わりに使われるメソッド名を表す Symbol オブジェクトを指定します。 実行結果に対して sym という名前のメソッドが呼ばれます。

例:

# 合計を計算する。
p [2, 3, 4, 5].inject {|result, item| result + item }        #=> 14

# 自乗和を計算する。初期値をセットする必要がある。
p [2, 3, 4, 5].inject(0) {|result, item| result + item**2 }  #=> 54
この式は以下のように書いても同じ結果が得られます。

result = 0
[1, 2, 3, 4, 5].each {|v| result += v }
p result   # => 15

p [1, 2, 3, 4, 5].inject(:+)                    #=> 15
p ["b", "c", "d"].inject("abbccddde", :squeeze) #=> "abcde"

引用:Ruby 2.5.0 リファレンスマニュアル#inject

injectで初期化した値が、ブロックの最初の要素 constant となり、配列 names がブロックの2番目の要素 name に入るという感じになります
names.inject(Object) do |constant, name|

ブロックの内部の処理をみてみます

if constant == Object
    constant.const_get(name)
else

ブロックの引数 constant がObjectクラスである場合、.const_get で nameで指定されている名前の定数を取り出す処理を行っています
次のループではconstant.const_get(name) の戻り値がconstantとして扱われます

次にelse以降をみていきます

else
    candidate = constant.const_get(name)
    next candidate if constant.const_defined?(name, false)

candidate変数にnameで指定されている名前の変数を取り出しております
次の行をみてみます

const_defined? の処理を調べてみると

モジュールに name で指定される名前の定数が定義されている時真 を返します。


スーパークラスや include したモジュールで定義された定数を検索対象 にするかどうかは第二引数で制御することができます。


[PARAM] name:

String, Symbol で指定される定数名。

[PARAM] inherit:

false を指定するとスーパークラスや include したモジュールで 定義された定数は対象にはなりません。


module Kernel
  FOO = 1
end

# Object は include したモジュールの定数に対しても
# true を返す
p Object.const_defined?(:FOO)   # => true

module Bar
  BAR = 1
end
class Object
  include Bar
end
# ユーザ定義のモジュールに対しても同様
p Object.const_defined?(:BAR)   # => true

class Baz
  include Bar
end
# Object 以外でも同様になった
# 第二引数のデフォルト値が true であるため
p Baz.const_defined?(:BAR)      # => true

# 第二引数を false にした場合
p Baz.const_defined?(:BAR, false)   # => false

引用:Ruby 2.5.0 リファレンスマニュアル#const_defined?

constantに定義されている名前か確認している処理のようです。更に第二引数がfalseの為、スーパークラスや includeしたモジュールに定義された定数は対象外としています
constantに定義されている場合は、constantを戻り値とし、次のループとなっています

次の行をみてみます

next candidate unless Object.const_defined?(name)

Objectクラスに定義された定数名でない場合には、candidateをそのまま戻り値として次のループとなっています

次をみてみるとinject内でさらにinjectしている処理があります

省略

# Go down the ancestors to check if it is owned directly. The check
# stops when we reach Object or the end of ancestors tree.
constant = constant.ancestors.inject(constant) do |const, ancestor|
    break const    if ancestor == Object
    break ancestor if ancestor.const_defined?(name, false)
    const
end

省略

継承チェーンの親要素までみて確認する処理のようです
ここの処理に行き着くconstantの条件は

  1. constantはObjectクラスではない
  2. constantオブジェクト自身に定義されている定数ではない(includeしているクラス, スーパークラスの定数は対象外)
  3. constantオブジェクトはObjectクラスに定義されている定数(Objectにincludeしているクラス, スーパークラスの定数も対象)

となっています

まず、ancestors メソッドを調べてみます

クラス、モジュールのスーパークラスとインクルードしているモジュール を優先順位順に配列に格納して返します。

module Foo
end
class Bar
  include Foo
end
class Baz < Bar
  p ancestors
  p included_modules
  p superclass
end
# => [Baz, Bar, Foo, Object, Kernel, BasicObject]
# => [Foo, Kernel]
# => Bar

引用:Ruby 2.5.0 リファレンスマニュアル#ancestors

constant.ancestors ではconstant変数のオブジェクトの継承チェーンを取り出しています

次にブロックの中をみてみます

break const    if ancestor == Object
break ancestor if ancestor.const_defined?(name, false)
const

継承チェーン内にObjectが含まれている場合は、constを戻り値とし、ループを終了しています 次の行をを見ると継承チェーンのオブジェクトancestorに定義された定数であった場合は、そのオブジェクトを戻り値とし、ループを終了します どの条件にもひっかからない場合は、更に継承チェーンをさかのぼって調べます

次をみていきます

# owner is in Object, so raise
constant.const_get(name, false)

さきほどの処理で取り出したconstantから定数を取り出しています