初めまして、yoshitamaです。
PHPをこれまで触っていましたが、Railsを触り始めて1ヶ月くらい経ちました。
今回はRailsについての理解を深めるため、Rails本体のコードを読んでみたのでその解説をしたいと思います。
私自身以前PHPのMVCフレームワークであるCakePHPを使っていて、初めてCakePHP本体のソースを読もうと思った時に、フレームワークのコードは難しくて自分のレベルではまだ読めないだろうと思っていました。
けれど、読んでみるとコード自体は普段使っている言語で書かれているものですし、少しづつ丁寧に追っていけば理解できることが多いです。(追いきれないこともありますが)
それに何より普段使っているものがどのような仕組みで動いているのかを紐解いていく過程は気づきや意外な発見があったり、なかなか楽しい作業です。
今回コードを読む箇所については、delegate
メソッドにしました。
delegate
メソッドとは
まずはdelegate
メソッドについての簡単な解説です。
ソースはここから抜粋(少し簡略化してます)。
class User < ActiveRecord::Base
#t.string :name
has_one :profile
end
class Profile < ActiveRecord::Base
#t.integer :age
#t.string :favorites
#t.string :location
belongs_to :user
delegate :name, to: :user
end
profile = Profile.find(1)
# 普通はuser を経由してから呼び出します
profile.user.name #=> "太郎"
# delegate するとprofile から直接呼び出せるようになります
profile.name #=> "太郎"
ProfieクラスからdelegateメソッドでUserオブジェクト(委譲先)にname
(委譲するメソッド)を指定することで、profile.name
のように、Profileクラスから直接Userオブジェクトのname
が呼び出せます。
また、delegateメソッドの引数にprefix
を指定するとメソッド名にプリフィックスをつけることができます。
今回の例だとprefix
に:hoge
を指定するとprofile.hoge_name
のような形で呼び出せます。
また、prefix
にtrue
を指定するとprofile.user_name
のように委譲先のオブジェクト名をプリフィックスとして呼び出せます。
以降のソースコード解説の際には適宜上記ProfileクラスとUserクラスを使用して説明します。
delegate
メソッドのソースコード
Rails本体のdelegate
メソッドのソースコードです。
Railsのバージョンは5.2.2。
delegate
メソッドが定義されているファイルはdelegation.rbになります。
def delegate(*methods, to: nil, prefix: nil, allow_nil: nil, private: nil)
unless to
raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter)."
end
if prefix == true && /^[^a-z_]/.match?(to)
raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method."
end
method_prefix = \
if prefix
"#{prefix == true ? to : prefix}_"
else
""
end
location = caller_locations(1, 1).first
file, line = location.path, location.lineno
to = to.to_s
to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to)
method_names = methods.map do |method|
# Attribute writer methods only accept one argument. Makes sure []=
# methods still accept two arguments.
definition = /[^\]]=$/.match?(method) ? "arg" : "*args, &block"
# The following generated method calls the target exactly once, storing
# the returned value in a dummy variable.
#
# Reason is twofold: On one hand doing less calls is in general better.
# On the other hand it could be that the target has side-effects,
# whereas conceptually, from the user point of view, the delegator should
# be doing one call.
if allow_nil
method_def = [
"def #{method_prefix}#{method}(#{definition})",
"_ = #{to}",
"if !_.nil? || nil.respond_to?(:#{method})",
" _.#{method}(#{definition})",
"end",
"end"
].join ";"
else
exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
method_def = [
"def #{method_prefix}#{method}(#{definition})",
" _ = #{to}",
" _.#{method}(#{definition})",
"rescue NoMethodError => e",
" if _.nil? && e.name == :#{method}",
" #{exception}",
" else",
" raise",
" end",
"end"
].join ";"
end
module_eval(method_def, file, line)
end
private(*method_names) if private
method_names
end
ソースコードの解説
詳細にコードを見て行く前に、delegate
メソッドが実際にやっていることは、呼び出し元に動的にメソッドを展開することです。
どういうことかというと、
class User < ActiveRecord::Base
has_one :profile
end
class Profile < ActiveRecord::Base
belongs_to :user
delegate :name, to: :user
end
上記コードが、
class User < ActiveRecord::Base
has_one :profile
end
class Profile < ActiveRecord::Base
belongs_to :user
def name
user.name
end
end
になるイメージです。
delegate
の部分が書き換えられています。
(わかりやすくするため、展開されたメソッドは簡略化しています。)
実際にname
メソッドを動的に生成することでProfileクラスからuser.name
がname
として呼び出せるわけです。
それではdelegate
メソッドのソースコードを見て行きます。
まずは冒頭部分。
def delegate(*methods, to: nil, prefix: nil, allow_nil: nil, private: nil)
unless to
raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter)."
end
if prefix == true && /^[^a-z_]/.match?(to)
raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method."
end
method_prefix = \
if prefix
"#{prefix == true ? to : prefix}_"
else
""
end
最初のunless to
は説明不要でしょう。to:
に何も指定していない場合に例外を投げています。
次はprefix
にtrue
を指定した場合のチェック。
「delegate
メソッドとは」の箇所で解説したように、prefix
にtrue
を指定すると委譲先のオブジェクト名をプリフィックスとして設定するのでした。
実際に、続くmethod_prefix
変数の定義箇所をみると、prefix
がtrue
の時はto
(委譲先)にアンダースコアを繋げたものを設定していることがわかります。
また、同時にif文の条件として指定されている/^[^a-z_]/.match?(to)
ですが、^[^a-z_]
の部分は正規表現で「小文字のaからzとアンダースコア以外から始まっている」を表しています。
つまりto
には小文字のaからzの小文字かアンダースコアから始まる文字を設定しないといけません。
prefix
がtrue
であれば、to
がメソッド名のプリフィックスになるので、to
に例えば数値で始まる文字列を指定してしまうとRubyのシンタックスエラーになってしまいますからそれを回避してるのだと思います。
次のコードを見ていきます。
location = caller_locations(1, 1).first
file, line = location.path, location.lineno
to = to.to_s
to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to)
初めの2行に関してはあとで実際に変数を利用している箇所でまとめて解説します。
そのあとに出てくるDELEGATION_RESERVED_METHOD_NAMES
ですが、定義は同じファイル内にあります。
RUBY_RESERVED_KEYWORDS = %w(alias and BEGIN begin break case class def defined? do
else elsif END end ensure false for if in module next nil not or redo rescue retry
return self super then true undef unless until when while yield)
DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block)
DELEGATION_RESERVED_METHOD_NAMES = Set.new(
RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
).freeze
DELEGATION_RESERVED_METHOD_NAMES
はどうやらrubyの予約語(RUBY_RESERVED_KEYWORDS
)に_ arg args block
の4つのキーワードを足したもののようです。
delegate
メソッド内のコードではDELEGATION_RESERVED_METHOD_NAMES
に委譲先のオブジェクト名が含まれていたらself.
を足しています。
これをしないと、委譲先のオブジェクト名がwhile
などのRubyの予約語だった場合に、Rubyのwhile
構文と解釈されシンタックスエラーになってしまいますのでself.
をつけているのだと思います。
いよいよメソッドの肝である部分である動的にメソッドを定義している箇所です。
method_names = methods.map do |method|
# Attribute writer methods only accept one argument. Makes sure []=
# methods still accept two arguments.
definition = /[^\]]=$/.match?(method) ? "arg" : "*args, &block"
# The following generated method calls the target exactly once, storing
# the returned value in a dummy variable.
#
# Reason is twofold: On one hand doing less calls is in general better.
# On the other hand it could be that the target has side-effects,
# whereas conceptually, from the user point of view, the delegator should
# be doing one call.
if allow_nil
method_def = [
"def #{method_prefix}#{method}(#{definition})",
"_ = #{to}",
"if !_.nil? || nil.respond_to?(:#{method})",
" _.#{method}(#{definition})",
"end",
"end"
].join ";"
else
exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
method_def = [
"def #{method_prefix}#{method}(#{definition})",
" _ = #{to}",
" _.#{method}(#{definition})",
"rescue NoMethodError => e",
" if _.nil? && e.name == :#{method}",
" #{exception}",
" else",
" raise",
" end",
"end"
].join ";"
end
module_eval(method_def, file, line)
end
この部分でやっていることは、
delegate
メソッドの呼び出し元で定義したいメソッドを文字列の形で生成してmethod_def
に格納し、それをmodule_eval
メソッドに渡すことでmethod_def
に格納された、文字列で表現されたメソッドがdelegate
メソッドの呼び出し元のメソッドのように定義されるということです。
それではソースの解説をしていきます。
definition = /[^\]]=$/.match?(method) ? "arg" : "*args, &block"
definition
変数は、後で出てくるdef #{method_prefix}#{method}(#{definition})
の部分で使われています。これはdelegateメソッドで定義されたメソッドを呼ぶ時に指定する引数のことです。
どういうことかというと、
class User < ActiveRecord::Base
has_one :profile
def hoge(foo)
# 何かの処理
end
end
class Profile < ActiveRecord::Base
belongs_to :user
delegate :hoge, to: :user
end
上記コードのように、委譲するメソッドが引数をとる場合に対応できるようにdefinition
で引数を指定できるようにしています。
definition
定義箇所の/[^\]]=$/
の=$
は「最後が=
で終わること」を表現していて、これがmethod
に対する条件になっています。最後が=
で終わるメソッドはセッターメソッドのことです。
また、[^\]]
は「]
ではない」を表現しているので]=
は許可しないということです。これは、コード内のコメントにもあるようにArrayクラスのメソッド[]=
を除いています。
つまり、definition
にはセッターメソッドであれば引数は一つなのでarg
、それ以外であれば*args, &block
を設定しています。
次のallow_nil
による分岐は、委譲先オブジェクトが指定メソッドを持っていない場合にNoMethodError
例外を発生させるかどうかです。
method_def
変数は先ほど解説したように、module_eval
に渡すとことで、それを呼び出し元に展開されたコードであるように定義してくれます。
module_eval
実行部分ですが、後回しにしていたcaller_locations
の箇所と一緒に見ていきます。
location = caller_locations(1, 1).first
file, line = location.path, location.lineno
(省略)
module_eval(method_def, file, line)
caller_location
を呼び出しているのはdelegateメソッドの呼び出し元のファイル名と行番号を取得するためです。
そしてmodule_eval
にそれらを渡すことで、delegate
メソッドに指定したメソッドの呼び出し時にエラーが発生した場合に、呼び出し元の情報をスタックトレースに出力することができます。
Profileクラスの例でいうと、profile.name
の呼び出しでエラーが発生した時に、delegate :name, to: :user
を定義したファイル名と行番号がスタックトレースに表示されます。
そして最後。
private(*method_names) if private
method_names
delegate
メソッドの引数のprivate
がtrue
の場合にprivateメソッドにmethod_names
を渡しています。
引数を持つprivateメソッドについて知らなかったので調べてみると、引数にメソッド名を渡すとその名前を持つメソッドをprivate扱いにしてくれるようです。
参考: private (Module)
そのあとのmethod_names
はdelegateメソッドの戻り値になります。
おわりに
以上がdelegate
メソッドの解説になります。
私と同じようにRailsを始めたてだったり、フレームワークのコードを読んでみたいけどレベル高くて読むのが大変そうと思っている人が興味を持ったり、読んでみるきっかけになれば嬉しいです。