普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました
読めるようにするまで
以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com
読んだ箇所
constantize
を今日は読んでみようと思います
どんな使い方だっけ?
読んでみる前にまずは使い方を調べてみます
Railsの日本語ドキュメントを見てみると
説明
引数の文字列で指定した名前で定数を探す
使い方
文字列.constantize()
例
'Module'.constantize # => Module 'Test::Unit'.constantize # => Test::Unit
ソースコードを読んでみる
1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def constantize
を探してみます
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
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するケースだと配列の先頭が空になる為、先頭の要素を取り除く処理を実行しています
※ 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"
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
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の条件は
- constantはObjectクラスではない
- constantオブジェクト自身に定義されている定数ではない(includeしているクラス, スーパークラスの定数は対象外)
- 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
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から定数を取り出しています