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

f:id:sktktk1230:20190921180106p:plain

普段仕事で使っている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
end

delegateを使用すればできるようになります。

class User < ApplicationRecord
  has_one :profile
 
  delegate :name, to: :profile
end

引用:RAILS GUIDES:delegate

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

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

f:id:sktktk1230:20181106163127p:plain

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

引用:Ruby2.5.0リファレンスマニュアル#caller_locations

まだ、いまいち理解が進まないので、更に調べます

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 `
'"]

引用:Ruby 2.0の機能解説メモ(Kernel)

メソッドの呼び出し元を調べる為に使うメソッドのようです
今回は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)