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

f:id:sktktk1230:20190921180106p:plain

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

読めるようにするまで

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

読んだ箇所

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

どんな使い方だっけ?

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

4.1.1 class_attribute

class_attributeメソッドは、1つ以上の継承可能なクラスの属性を宣言します。そのクラス属性は、その下のどの階層でも上書き可能です。

class A
  class_attribute :x
end

class B < A; end

class C < B; end

A.x = :a
B.x # => :a
C.x # => :a

B.x = :b
A.x # => :a
C.x # => :b

C.x = :c
A.x # => :a
B.x # => :b

引用:RAILS GUIDES:class_attribute

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

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

f:id:sktktk1230:20190529145022p:plain

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

1. activesupport > lib > active_support > core_ext > class > attribute.rb
# frozen_string_literal: true

require "active_support/core_ext/kernel/singleton_class"
require "active_support/core_ext/module/redefine_method"
require "active_support/core_ext/array/extract_options"

class Class

省略

  def class_attribute(
    *attrs,
    instance_accessor: true,
    instance_reader: instance_accessor,
    instance_writer: instance_accessor,
    instance_predicate: true,
    default: nil
  )
    attrs.each do |name|
      singleton_class.silence_redefinition_of_method(name)
      define_singleton_method(name) { default }

      singleton_class.silence_redefinition_of_method("#{name}?")
      define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate

      ivar = "@#{name}".to_sym

      singleton_class.silence_redefinition_of_method("#{name}=")
      define_singleton_method("#{name}=") do |val|
        redefine_singleton_method(name) { val }

        if singleton_class?
          class_eval do
            redefine_method(name) do
              if instance_variable_defined? ivar
                instance_variable_get ivar
              else
                singleton_class.send name
              end
            end
          end
        end
        val
      end

      if instance_reader
        redefine_method(name) do
          if instance_variable_defined?(ivar)
            instance_variable_get ivar
          else
            self.class.public_send name
          end
        end

        redefine_method("#{name}?") { !!public_send(name) } if instance_predicate
      end

      if instance_writer
        redefine_method("#{name}=") do |val|
          instance_variable_set ivar, val
        end
      end
    end
  end

まず class_attribute の引数からみてみます

  def class_attribute(
    *attrs,
    instance_accessor: true,
    instance_reader: instance_accessor,
    instance_writer: instance_accessor,
    instance_predicate: true,
    default: nil
  )

attrs は可変長引数で instance_accessor などの引数はキーワード引数です
可変長引数は引数を配列として受け取れ、キーワード引数はハッシュとして引数で宣言できます

参照:Rubyの引数いろいろ - Qiita

それぞれ以下のように挙動します

class A
  def self.hoge(*attrs, huga: 'a')
    p '==attrs=='
    p attrs
    p '==huga=='
    p huga
  end
end

=> A.hoge(1,2,3, huga: 'b')
"==attrs=="
[1, 2, 3]
"==huga=="
"b"

それではメソッドの中身を見ていきます

attrs.each do |name|
  singleton_class.silence_redefinition_of_method(name)
  define_singleton_method(name) { default }

  singleton_class.silence_redefinition_of_method("#{name}?")
  define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate

  ivar = "@#{name}".to_sym

attrs は先程の可変長引数で配列になります。配列の要素を each で1つずつ取り出して処理しています

each の中の処理を見ていきます
singleton_class.silence_redefinition_of_method(name)
singleton_class メソッドでレシーバの特異クラスを戻り値とします

参照:instance method Object#singleton_class (Ruby 2.6.0)

silence_redefinition_of_methodactive_support > core_ext > module > redefine_method.rb に定義されているメソッドです
処理内容と経緯については @y-yagi さんのブログに記載がありましたので、こちらを引用させて頂きます

普通にメソッドを再定義しようとすると既にメソッドがある為Rubyのwarningが発生します。それを避ける為、今まではremove_possible_methodメソッドを先に呼び出してメソッドを未定義にするようにしていました(redefine_methodメソッド内でもそのような挙動になっています)。が、メソッドを再定義する、という事をする為にメソッドを未定義にする、というのは目的がわかりにくいのでは、という事で新たにremove_possible_methodを使用せずに再定義するメソッドが追加されました。

実装は下記の通りです(Ruby 2.3以降の場合)。

# Marks the named method as intended to be redefined, if it exists.
# Suppresses the Ruby method redefinition warning. Prefer
# #redefine_method where possible.
def silence_redefinition_of_method(method)
  if method_defined?(method) || private_method_defined?(method)
    # This suppresses the "method redefined" warning; the self-alias
    # looks odd, but means we don't need to generate a unique name
    alias_method method, method
  end
end

alias_methodを使用して対応しているんですねえ。

引用:rails commit log流し読み(2017/09/01)

処理自体はメソッドの再定義をRubyのエラーを表示させず、実行できるものです

次の行を見てみます

  define_singleton_method(name) { default }

define_singleton_method は、レシーバにシングルトンのメソッドを追加できるメソッドです
使い方は以下の通りです

define_singleton_method(symbol, method) → symbol
define_singleton_method(symbol) { block } → symbol

引用:Rubyドキュメント2.6

define_singleton_method の引数とブロック内の default にそれぞれ何が入るかは name は、可変長引数 attrs の各要素で、ブロック内の default は引数のdefaultが入ります

  def class_attribute(
    *attrs,
    instance_accessor: true,
    instance_reader: instance_accessor,
    instance_writer: instance_accessor,
    instance_predicate: true,
    default: nil
  )
    attrs.each do |name|
      singleton_class.silence_redefinition_of_method(name)
      define_singleton_method(name) { default }

次の行をみます
先程見てきた処理とほとんど一緒です

  singleton_class.silence_redefinition_of_method("#{name}?")

違いは name? にしているところです
rubyで ? が付くメソッドは、booleanを返すメソッドです

次の行をみます

  define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate

instance_predicate はメソッドの引数の値でデフォルト値が true のものです
define_singleton_method("#{name}?") は、先ほどと一緒でシングルトンメソッドを定義している処理です
ブロック内の処理 { !!public_send(name) } を見ていきます

public_send がどんな処理をするか見てみると、引数のパブリックメソッドを呼び出す処理です

オブジェクトの public メソッド name を args を引数にして呼び出し、メソッ ドの実行結果を返します。

1.public_send(:+, 2)  # => 3

引用:Ruby 2.6.0 リファレンスマニュアル

public_sendの前にある !! はdouble bangという方法で 戻り値を booleanにする方法です

参照:Rubyの否定演算子2つ重ね「!!」(double-bang)でtrue/falseを返す

ちなみにrubocopではチェック対象になります

Examples:

# bad
!!something

# good
!something.nil?

引用:Class: RuboCop::Cop::Style::DoubleNegation

次の行をみます
インスタンス変数名をシンボルにしています
例えば、name が hoge な場合だと、 :@hoge となります

ivar = "@#{name}".to_sym

次の行をみます

singleton_class.silence_redefinition_of_method("#{name}=")

今までと同じようにこんどはsetterのメソッド定義しています

まずは、次の処理の全体像をみます

  define_singleton_method("#{name}=") do |val|
    redefine_singleton_method(name) { val }

    if singleton_class?
      class_eval do
        redefine_method(name) do
          if instance_variable_defined? ivar
            instance_variable_get ivar
          else
            singleton_class.send name
          end
        end
      end
    end
    val
  end

define_singleton_method("#{name}=") は今までと同じでシングルトンメソッドの定義です

次の行をみます

redefine_singleton_method(name) { val }

redefine_singleton_method は既に定義されているメソッドを上書きするRailsのメソッドです

参照:Module

次のif文内の全体像をみます

if singleton_class?
  class_eval do
    redefine_method(name) do
      if instance_variable_defined? ivar
        instance_variable_get ivar
      else
        singleton_class.send name
      end
    end
  end
end

最初の行は、レシーバがシングルトンのクラスかどうか判定しています

if singleton_class?

次の行をみます

class_eval do

省略

end

class_eval のブロック内の処理をレシーバに動的に実装します

モジュールのコンテキストで文字列 expr またはモジュール自身をブロックパラメータとするブロックを 評価してその結果を返します。 モジュールのコンテキストで評価するとは、実行中そのモジュールが self になるということです。 つまり、そのモジュールの定義式の中にあるかのように実行されます。

引用:Ruby 2.6.0 リファレンスマニュアル#class_eval

class_eval のブロック内を見ていきます

redefine_method(name) do
  if instance_variable_defined? ivar
    instance_variable_get ivar
  else
    singleton_class.send name
  end
end

redefine_methodactivesupport > lib > active_support > core_ext > class > redefine_method.rb の17行目に定義されているメソッドで定義済みのメソッドを書き換える処理を行います

instance_variable_defined? は引数のインスタンス変数名が定義されていれば、真を返すメソッドです

インスタンス変数 var が定義されていたら真を返します。 引用:Ruby 2.6.0 リファレンスマニュアル#instance_variable_defined?

ivarclass_attribute メソッドの引数 *attrs の要素の1つになります

  def class_attribute(
    *attrs,
    instance_accessor: true,
    instance_reader: instance_accessor,
    instance_writer: instance_accessor,
    instance_predicate: true,
    default: nil
  )
    attrs.each do |name|
    
    省略
    
      ivar = "@#{name}".to_sym

if instance_variable_defined? ivar が真の場合は、instance_variable_get ivar でレシーバのインスタンス変数を取り出します

オブジェクトのインスタンス変数の値を取得して返します。 インスタンス変数が定義されていなければ nil を返します。 引用:Ruby 2.6.0 リファレンスマニュアル#instance_variable_get

偽の場合は、singleton_class.send name でレシーバから特異クラスを返し、特異クラスに対して send namename メソッドを呼び出します

レシーバの特異クラスを返します。 まだ特異クラスがなければ、新しく作成します。 レシーバが nil か true か false なら、それぞれ NilClass, TrueClass, FalseClass を返します。 引用:Ruby 2.6.0 リファレンスマニュアル#singleton_class

次にこちらをみます
今まで見てきた実装とほぼ同じです
ことなる部分は、 if instance_variable_defined?(ivar) が偽の場合に、 self.class.public_send name でレシーバのクラスメソッドを呼び出す処理になっている部分です

instance method Object#public_send (Ruby 2.6.0)

  if instance_reader
    redefine_method(name) do
      if instance_variable_defined?(ivar)
        instance_variable_get ivar
      else
        self.class.public_send name
      end
    end

    redefine_method("#{name}?") { !!public_send(name) } if instance_predicate
  end

if instance_predicate が真の場合は、レシーバのpublicなメソッドとして定義されている name を定義しています
例えば、 namehoge である場合は、 hoge? というメソッドが定義され、 戻り値が true, false になります

次をみていきます
instance_writer が真の場合に、redefine_method でセッターを定義しています

  if instance_writer
    redefine_method("#{name}=") do |val|
      instance_variable_set ivar, val
    end
  end

参照:instance method Object#instance_variable_set (Ruby 2.6.0)