普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました
読めるようにするまで
以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com
読んだ箇所
delegate
を今日は読んでみようと思います
どんな使い方だっけ?
読んでみる前にまずは使い方を調べてみます
RAILS GUIDESの日本語ドキュメントを見てみると
elegateマクロを使用すると、メソッドを簡単に委譲できます。
あるアプリケーションのUserモデルにログイン情報があり、それに関連する名前などの情報はProfileモデルにあるとします。
class User < ApplicationRecord has_one :profile endこの構成では、user.profile.nameのようにプロファイル越しにユーザー名を取得することになります。これらの属性に直接アクセスできたらもっと便利になることでしょう。
class User < ApplicationRecord has_one :profile def name profile.name end enddelegateを使用すればできるようになります。
class User < ApplicationRecord has_one :profile delegate :name, to: :profile end
ソースコードを読んでみる
1. railsプロジェクトのactivesupportにある機能ですので、activesupportディレクトリのlib配下で def delegate
を探してみます
2. 該当箇所が10個ほどあったので、それぞれみてみます
1. activesupport > lib > active_support > core_ext > module > delegation.rb
# frozen_string_literal: true require "set" class Module # Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+ # option is not used. 省略 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
まずはじめにここまで見てみます
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
toはdelegateメソッドの引数の為、引数toが偽の場合はArgumentErrorになるようにチェックしています
次を見ていきます
if prefix == true && /^[^a-z_]/.match?(to) raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method." end
if文内の正規表現 /^[^a-z_]/
は、preifxがtrueかつ引数toの先頭の文字列が英字と_以外だった場合にマッチします
こちらの条件に当たる場合は、エラーになります
※正規表現について
次にこちらを見ていきます
method_prefix = \ if prefix "#{prefix == true ? to : prefix}_" else "" end
まず、こちらの書き方ですとif文内の戻り値が変数に格納されるという動きをします
変数 = if 〜 else 〜 end
これを踏まえて見ていきます
まず、prefixが真の場合です
"#{prefix == true ? to : prefix}_"
prefixがtrueの場合は、引数toに _ がついた文字列が戻り値になります
prefixがtrue以外の場合は、prefixに _ がついた文字列が戻り値となります
prefixが偽の場合は、空文字が戻り値となります
次を見ていきます
location = caller_locations(1, 1).first file, line = location.path, location.lineno
caller_locationsがわからなかったので調べてみます
現在のフレームを Thread::Backtrace::Location の配列で返します。引 数で指定した値が範囲外の場合は nil を返します。
[PARAM] start:
開始フレームの位置を数値で指定します。
[PARAM] length:
取得するフレームの個数を指定します。
[PARAM] range:
取得したいフレームの範囲を示す Range オブジェクトを指定します。
[SEE_ALSO] Thread::Backtrace::Location, Kernel.#caller
まだ、いまいち理解が進まないので、更に調べます
Kernel#caller_locations
スタックに積まれたメソッドの呼び出し元を配列として返してくれる。
1 def fiz 2 baz 3 end 4 5 def baz 6 p caller_locations 7 end 8 9 fiz% ruby caller_locations.rb ["caller_locations.rb:2:in `fiz'", "caller_locations.rb:9:in ` '"]
メソッドの呼び出し元を調べる為に使うメソッドのようです
今回はdelegateメソッド呼び出し元を調べているという感じです
location = caller_locations(1, 1).first file, line = location.path, location.lineno
呼び出し元の情報を取得して、fileと行数をそれぞれ変数に格納しています
次を見てみます
to = to.to_s to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to)
引数toを文字列としています
次を見ると
if DELEGATION_RESERVED_METHOD_NAMES.include?(to)
toがDELEGATION_RESERVED_METHOD_NAMES
に含まれているかチェックしています
定数の定義場所を見ているとこちらになります
class Module 省略 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
こちらに該当する文字列であった場合は、レシーバのメソッドが呼ばれるという動きになります
次の処理を見ていきます
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
methodsはdelegateメソッドの引数です
まず最初の処理を見ていきます
# Attribute writer methods only accept one argument. Makes sure []= # methods still accept two arguments. definition = /[^\]]=$/.match?(method) ? "arg" : "*args, &block"
method名に]が含まれていないメソッドが確認しています
次を見ていきます
# 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
allow_nilが真の場合は、method_def変数に配列内の文字列を ; で連結し、文字列として格納しています
method_def変数の文字列は動的にメソッド定義されるときに使われます
そのため、どのような処理のメソッドが定義されるのか見てみます
まず配列の最初はメソッドの定義部分です
"def #{method_prefix}#{method}(#{definition})",
例えば、prefixがhogeの場合は、 def hoge_huga(arg)
みたいになります
次は、toをローカル変数 _ に格納しています
"_ = #{to}",
その次は、ローカル変数 _ がnilではない または、nilがmethodを実装している場合に真となります
"if !_.nil? || nil.respond_to?(:#{method})",
※ respond_to?について
instance method Object#respond_to? (Ruby 2.6.0)
真だった場合には、変数 _ にメソッド定義部分で使用した変数 methodと仮引数で使用したdefinitionを使い呼び出しています
" _.#{method}(#{definition})"
例えば、prefixがhoge, methodがhugaの場合は、このようなメソッド def hoge_huga(arg)
を定義することになるのですが、
内部での呼び出しは、このような形になります
to.huga(arg)
次に、こちらを見ていきます
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
こちらはさきほどとの違いはエラー処理が追加されているところです
exception変数にはエラーメッセージが配列で格納されています
さきほどと同じようにmethod_defには動的にメソッド定義する処理が記載されています
NoMethodErrorが発生し、delegateメソッドの引数toがnilかつエラーが発生したメソッド名がmethodと同じであれば、delegationErrorが発生するという処理が先程と異なっています
"rescue NoMethodError => e", " if _.nil? && e.name == :#{method}", " #{exception}", " else", " raise", " end",
これでmethod_def変数の戻り値の部分は見終わりました
次を見ていくと、ファイルと行番号を指定して、メソッド定義を動的に行っています
module_eval(method_def, file, line)
※ module_evalについて
instance method Module#class_eval (Ruby 2.6.0)
ブロック内は見終わったので、次を見ていきます
private(*method_names) if private method_names
delegateメソッドの引数privateが真の場合は、method_namesで定義したメソッドをprivateに設定します
※ privateについて
instance method Module#private (Ruby 2.6.0)