普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました
読めるようにするまで
以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com
読んだ箇所
parent
を今日は読んでみようと思います
どんな使い方だっけ?
読んでみる前にまずは使い方を調べてみます
Railsの日本語ドキュメントを見てみると
parentメソッドは、名前がネストしたモジュールに対して実行でき、対応する定数を持つモジュールを返します。
module X module Y module Z end end end M = X::Y::Z X::Y::Z.parent_name # => "X::Y" M.parent_name # => "X::Y"
ソースコードを読んでみる
1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def parent
を探してみます
2. 該当箇所が1箇所だったので、みてみます
1. activesupport > lib > active_support > core_ext > module > introspection.rb
省略 # Returns the module which contains this one according to its name. # # module M # module N # end # end # X = M::N # # M::N.parent # => M # X.parent # => M # # The parent of top-level and anonymous modules is Object. # # M.parent # => Object # Module.new.parent # => Object def parent parent_name ? ActiveSupport::Inflector.constantize(parent_name) : Object end 省略
三項演算子 xxx ? yyy : zzz
を使っています
parent_name
の戻り値によって、
真の場合は ActiveSupport::Inflector.constantize(parent_name)
偽の場合は Object
となるようです
parent_name
の実装をみてみます
# frozen_string_literal: true require "active_support/inflector" class Module # Returns the name of the module containing this one. # # M::N.parent_name # => "M" def parent_name if defined?(@parent_name) @parent_name else parent_name = name =~ /::[^:]+\Z/ ? $`.freeze : nil @parent_name = parent_name unless frozen? parent_name end end 省略
Moduleクラスをオープンクラスし、parent_name
メソッドを実装しています
まずは最初のif文をみてみると
if defined?(@parent_name) @parent_name else
変数@parent_nameが定義されている場合はそのまま変数を戻り値とします
変数@parent_nameが定義されていない場合の処理は以下になります
else parent_name = name =~ /::[^:]+\Z/ ? $`.freeze : nil @parent_name = parent_name unless frozen? parent_name end
こちらからみていきます
name =~ /::[^:]+\Z/
Moduleクラスのnameメソッドの仕様を調べてみると
モジュールやクラスの名前を文字列で返します。
このメソッドが返す「モジュール / クラスの名前」とは、 より正確には「クラスパス」を指します。 クラスパスとは、ネストしているモジュールすべてを 「::」を使って表示した名前のことです。 クラスパスの例としては「CGI::Session」「Net::HTTP」が挙げられます。
[RETURN]
名前のないモジュール / クラスに対しては nil を返します。
module A module B end p B.name #=> "A::B" class C end end p A.name #=> "A" p A::B.name #=> "A::B" p A::C.name #=> "A::C" # 名前のないモジュール / クラス p Module.new.name #=> nil p Class.new.name #=> nil
クラスパスを戻り値をするメソッドのようです
=~
も調べてみます
正規表現 other とのマッチを行います。 マッチが成功すればマッチした位置のインデックスを、そうでなければ nil を返します。
other が正規表現でも文字列でもない場合は other =~ self を行います。
このメソッドが実行されると、組み込み変数 $~, $1, ... にマッチに関する情報が設定されます。
[PARAM] other:
正規表現もしくは =~ メソッドを持つオブジェクト
[EXCEPTION] TypeError:
other が文字列の場合に発生します。
例 p "string" =~ /str/ # => 0 p "string" =~ /not/ # => nil p "abcfoo" =~ /foo/ # => 3
正規表現にパターンマッチした情報を$1などに格納する処理でマッチした場合は、Integerクラスが戻り値となるようです
正規表現をみてみると ::[^:]+\Z/
::ではじまり[^:]+
:にマッチしない1回以上の繰り返しを末尾とする文字にマッチするようです
三項演算子の真の場合の処理 $`.freeze
をみてみます
現在のスコープで最後に成功した正規表現のパターンマッチでマッチした 部分より後ろの文字列です。 最後のマッチが失敗していた場合には nil となります。
Regexp.last_match.post_match と同じです。
この変数はローカルスコープかつスレッドローカル、読み取り専用です。 Ruby起動時の初期値は nil です。
例 str = '<p><a href="http://example.com">example.com</a></p>' if %r[<a href="(.*?)">(.*?)</a>] =~ str p $' end #=> "</p>"
パターンマッチした部分より、後ろの文字列を戻り値とし、それを.freeze
で変更不可としています
ここまでの処理をサンプルのクラスで実行し、動きをみてみました
次に @parent_name = parent_name unless frozen?
をみます
frozen?
でレシーバが変更不可状態がチェックしています
理由がわからなかった為、コミットログから探してみます
こちらのコミットが該当のコミットとイシューのようです
プルリクエスト
github.com
イシュー
github.com
イシューをみてみると
オブジェクトをfreezeした状態でインスタンス変数に変更を加えてしまうとエラーとなる為、回避しているようです
frozen?
をしている理由がわかったので、 unless の条件文内部の処理をみてみます
@parent_name = parent_name
こちらの処理でインスタンス変数@parent_nameににいれることでキャッシュとして扱えるようにしています
2回以降の parent_name
メソッドを呼ぶと 一度目の処理でインスタンス変数は初期化済みの為、if defined?(@parent_name)
がtrueとなるので、else以降の処理は呼ばれず、インスタンス変数を返す処理になります
parent_nameメソッドの処理は読めたので、 parent
メソッドに戻ります
省略 # Returns the module which contains this one according to its name. # # module M # module N # end # end # X = M::N # # M::N.parent # => M # X.parent # => M # # The parent of top-level and anonymous modules is Object. # # M.parent # => Object # Module.new.parent # => Object def parent parent_name ? ActiveSupport::Inflector.constantize(parent_name) : Object end 省略
parent_nameの戻り値が真だった場合をみてみます
ActiveSupport::Inflector.constantizeを調べると
Tries to find a constant with the name specified in the argument string.
constantize('Module') # => Module constantize('Foo::Bar') # => Foo::BarThe 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
引数で指定した名前の定数を探すメソッドのようです
<追記>
constantizeもコードリーディングしてみました
blog.shitake4.tech
なので、ActiveSupport::Inflector.constantize(parent_name)
は parent_nameの戻り値の定数を返します
偽の場合は、オブジェクトを戻り値としています