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

f:id:sktktk1230:20180726124729p:plain

普段仕事で使っているRuby on Railsですが、ソースコードを読む機会もなかなかないので、試しにやってみることにしました

読めるようにするまで

以前書いた記事で読めるようにするまでの設定を画像キャプチャ付きで解説しましたので、よろしければこちらをご参照下さい
shitake4.hatenablog.com

読んだ箇所

mattr_reader を今日は読んでみようと思います

どんな使い方だっけ?

読んでみる前にまずは使い方を調べてみます
Railsの日本語ドキュメントを見てみると

3.2.3 モジュール属性

mattr_reader、mattr_writer、mattr_accessorという3つのマクロは、クラス用に定義されるcattr_*マクロと同じです。実際、cattr_*マクロは単なるmattr_*マクロの別名です。クラス属性も参照してください。

たとえば、これらのマクロは以下のDependenciesモジュールで使用されています。

module ActiveSupport
  module Dependencies
    mattr_accessor :warnings_on_first_load
    mattr_accessor :history
    mattr_accessor :loaded
    mattr_accessor :mechanism
    mattr_accessor :load_paths
    mattr_accessor :load_once_paths
    mattr_accessor :autoloaded_constants
    mattr_accessor :explicitly_unloadable_constants
    mattr_accessor :logger
    mattr_accessor :log_activity
    mattr_accessor :constant_watch_stack
    mattr_accessor :constant_watch_stack_mutex
  end
end  

引用:RAILS GUIDE#モジュール属性

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

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

f:id:sktktk1230:20180709124745p:plain

2. 該当箇所が1箇所あったので、みてみます

1. activesupport > lib > active_support > core_ext > module > attribute_accessors.rb
def mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil)
   syms.each do |sym|
     raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)
     class_eval(<<-EOS, __FILE__, __LINE__ + 1)
       @@#{sym} = nil unless defined? @@#{sym}
       def self.#{sym}
         @@#{sym}
       end
     EOS

     if instance_reader && instance_accessor
       class_eval(<<-EOS, __FILE__, __LINE__ + 1)
         def #{sym}
           @@#{sym}
         end
       EOS
     end

     sym_default_value = (block_given? && default.nil?) ? yield : default
     class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil?
   end
 end
 alias :cattr_reader :mattr_reader

まずここまでの処理を見てみます

syms.each do |sym|
  raise NameError.new("invalid attribute name: #{sym}") unless /\A[_A-Za-z]\w*\z/.match?(sym)

symsはsymbolsを短縮した名前で、変数に入る内容は、シンボルの配列ではないかと推測できます
unless 以下の正規表現 /\A[_A-Za-z]\w*\z/ を見てみると
\A は文字列の先頭にマッチ
[_A-Za-z] は指定された文字のどれかに一致
\w は英単語を構成する文字(a~z,A~Z,_,1~9)
* は0回以上の繰り返しにマッチ
\z は文字列の末尾にマッチ

上記の条件に当てはまるシンボルにマッチするかを検査しています
例えば、_hoge, Huga1 などがマッチします

これにマッチしない場合は NameError 例外を発生させるということになります

正規表現について詳しくは以下を参照

qiita.com

docs.ruby-lang.org

マッチした場合は、以下の処理が実行されます

class_eval(<<-EOS, __FILE__, __LINE__ + 1)
  @@#{sym} = nil unless defined? @@#{sym}
  def self.#{sym}
    @@#{sym}
  end
EOS

class_evalにより引数のヒアドキュメントの内容をクラス内でそのまま実行することになります  

モジュールのコンテキストで文字列 expr またはモジュール自身をブロックパラメータとするブロックを 評価してその結果を返します。

モジュールのコンテキストで評価するとは、実行中そのモジュールが self になるということです。 つまり、そのモジュールの定義式の中にあるかのように実行されます。

ただし、ローカル変数は module_eval/class_eval の外側のスコープと共有します。

文字列が与えられた場合には、定数とクラス変数のスコープは自身のモジュール定義式内と同じスコープになります。 ブロックが与えられた場合には、定数とクラス変数のスコープはブロックの外側のスコープになります。

class C
end
a = 1
C.class_eval %Q{
  def m                   # メソッドを動的に定義できる。
    return :m, #{a}
  end
}

p C.new.m        #=> [:m, 1]

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

ヒアドキュメントの処理をそれぞれ見ていきます
@@#{sym} = nil unless defined? @@#{sym} では変数symのクラス変数が自クラスに定義されているかを確認しています
定義されていない場合は、クラス変数にnilを代入しています

そして、こちらではクラス変数のアクセッサ(クラスメソッド)を定義しています

def self.#{sym}
  @@#{sym}
end

次にこちらを見ていきます

if instance_reader && instance_accessor
  class_eval(<<-EOS, __FILE__, __LINE__ + 1)
    def #{sym}
      @@#{sym}
    end
  EOS
end

instance_reader && instance_accessor こちらはメソッドの引数の値によって、判定されます。(デフォルトはどちらもtrueの為、実行されます)

  def mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil)

こちらの記述は先程のクラス変数のアクセッサとほぼ同一ですが、こちらはクラスメソッドではなく、インスタンスメソッドになります

class_eval(<<-EOS, __FILE__, __LINE__ + 1)
  def #{sym}
    @@#{sym}
  end
EOS

クラスメソッド、インスタンスメソッドの定義の方法については以下参照

qiita.com

次にこちらを見ていきます

sym_default_value = (block_given? && default.nil?) ? yield : default
class_variable_set("@@#{sym}", sym_default_value) unless sym_default_value.nil?

ブロックが与えられているかつメソッドの引数defaultがnilの場合に、ブロックの返り値をsym_default_value変数に代入しています
それ以外の場合に、メソッドの仮引数のdefaultの値を代入しています

sym_default_valuenilでないときに、クラス変数に値を代入しています

mattr_readerにはエイリアスも設定されており、cattr_readerも同じ処理を行うことができます
alias :cattr_reader :mattr_reader