その他

【ソースコードリーディング】attr_internal編【Active Support】

最近、Railsのソースコードを読むようにしています。Railsなんかは難しくてなかなか詳細つかみにくいですが、ActiveSupportのメソッドなんかはその中でも比較的分かりやすかったです。今回はattr_internalについて読んだのでそれについてメモしていきたいと思います。

attr_internalとは

Railsガイドにも説明がありましたが、分かりにくかったので自分なりに噛み砕いて書いていきます。

# ライブラリ
class ThirdPartyLibrary::Crawler
  attr_internal :log_level
end

# クライアントコード
class MyCrawler < ThirdPartyLibrary::Crawler
  attr_accessor :log_level
end

ライブラリの中で@log_levelの参照や書き込みができるように定義されています。このライブラリのクラスを継承したり、ミックスインして同じ名前のアクセサーを定義した場合どうなるでしょうか。クライアントコードの中で、開発者は知らず知らずの内にインスタンス変数を上書きして、ライブラリのコードを壊してしまう可能性があります。それを防いでくれるのがattr_internalです。attr_accesorと同じようにセッターとゲッターを定義しますが、中のインスタンス変数を@_log_levelというようにクライアントコード側で被らないように工夫がされます。では、ソースコードを見ていきます。

ソースコードを読む

attr_internalメソッドはactive_support/core_ext/module/attr_internal.rbにあります。

まず、attr_internalメソッドはattr_internal_accessorのエイリアスであることが分かります。

alias_method :attr_internal, :attr_internal_accessor

それではattr_internal_accessorメソッドを見てみます。
attr_internal_readerメソッドとattr_internal_writerメソッドを呼び出しているだけですね。

def attr_internal_accessor(*attrs)
    attr_internal_reader(*attrs)
    attr_internal_writer(*attrs)
  end

まずは、attr_internal_readerメソッドからです。
ここではなにやら引数に渡された可変長引数をイテレートして、attr_internal_defineメソッドに渡しているようです。

def attr_internal_reader(*attrs)
    attrs.each { |attr_name| attr_internal_define(attr_name, :reader) }
  end

ここがメインの処理のようです。attr_nameには:log_levelが、typeには:readerが割り当てられています。
2行目でattr_internal_ivar_nameメソッドを呼び出していますね。

  def attr_internal_define(attr_name, type)
      internal_name = attr_internal_ivar_name(attr_name).delete_prefix("@")
      # use native attr_* methods as they are faster on some Ruby implementations
      public_send("attr_#{type}", internal_name)
      attr_name, internal_name = "#{attr_name}=", "#{internal_name}=" if type == :writer
      alias_method attr_name, internal_name
      remove_method internal_name
    end

attr_internal_ivar_nameメソッドでなにやらModuleの特異メソッドを呼び出しています。

def attr_internal_ivar_name(attr)
  Module.attr_internal_naming_format % attr
end

特異メソッドの定義を見つけました。ここでselfを拡張してアクセサーを定義していますね。余談ですがクラスへの特異メソッドの定義はクラスメソッドの定義と同義なので、class << self; endがなくても動きます。
そして、アクセサーに”@_%s”を返すようにセットしています。

class << self
  # この箇所はModuleの定義のときに呼ばれる
  # selfはModule
  attr_accessor :attr_internal_naming_format
end

self.attr_internal_naming_format = "@_%s"

つまり、Module.attr_internal_naming_format % attrは@_log_levelを返します。あまり見慣れないですが、String#%を使っています。

p "i = %d" % 10       # => "i = 10"
p "i = %s" % 'hoge'       # => "i = hoge""

ここまでで、もとのattr_nameの先頭に_をつけるところまで来ました。

次にpublic_send(~~)でModuleをレシーバとしてattr_readerメソッドを引数に_log_levelを指定して呼び出しています。そうして生えたinternal_nameメソッドをattr_nameという名前でメソッドをコピーして、最後にinternal_nameメソッド(_log_level)を削除しています。

public_send("attr_#{type}", internal_name)
attr_name, internal_name = "#{attr_name}=", "#{internal_name}=" if type == :writer
alias_method attr_name, internal_name
remove_method internal_name

こうすることで、log_levelというインターフェイスを残しつつ内部で使われるインスタンス変数は@_log_levelにするということを実現していたのですね。
attr_writerについてはほとんど同じなので割愛します。

さいごに

なにかライブラリをつくるときにはこのattr_internalメソッドを使わないと使う側で予期せぬバグが起きそうなので、積極的に使っていきたいと思いました。
ここまで読んでいただきありがとうございました。

ABOUT ME
酒井 駿
名古屋工業大学大学院卒業後、豊田合成(株)で品質管理を経験し、その後スタートアップ・マネーフォワードを経て、2024年11月に株式会社EGGHEAD創業。 製造業とエンジニアリング、両方の現場の知見を活かし、製造業における生成AIを活用した業務改善やシステム開発を支援します。