2021-08-30

Railsで見つけた気になったコード

僕はそこそこ長いことRailsで開発をしていて、今まで色んな会社で色んなコードを見てきたけど、Railsのソースコードを読んでいるとパッと見ただけでは説明できないコードと遭遇する
Ruby大好きで読めないコードがあるのはとても悔しいので、しっかり読んで意味を理解したいと思う
世界最高のRubyist達が書いたコードを読んでRuby力を上げていきましょう

define_callbacksメソッド

まずはactivesupportのdefinecallbacksメソッドのこの部分↓

module_eval <<-RUBY, __FILE__, __LINE__ + 1
  def _run_#{name}_callbacks(&block)
    run_callbacks #{name.inspect}, &block
  end

  def self._#{name}_callbacks
    get_callbacks(#{name.inspect})
  end

  def self._#{name}_callbacks=(value)
    set_callbacks(#{name.inspect}, value)
  end

  def _#{name}_callbacks
    __callbacks[#{name.inspect}]
  end
RUBY

ヒアドキュメントを使っているのはわかるが、 __FILE__ とか __LINE__ ってなんのためにあるんだろうか?(しかも + 1 とは…?_?)

<<-RUBY とハイフンを入れているのは、終端行の RUBY の前にインデントを入れるためであることはわかる
https://docs.ruby-lang.org/ja/latest/doc/spec=2fliteral.html#here

その後の __FILE__ などはmodule_evalのドキュメントを見たら理解できた

module_eval は3つの引数を受け取ることができて、上のコードでは

  • 第1引数に <<-RUBY〜RUBY までのヒアドキュメントの内容(すなわちメソッド定義)
  • 第2引数に __FILE__
  • 第3引数に __LINE__

を受け取っている
ヒアドキュメントの挙動を小さいコードで確認してみるとより理解することができる↓

module Hakozaru2
  def self.hoge
    fuga 123, <<-RUBY, "arg3"
      ヒアドキュメントの内容
    RUBY
  end

  def self.fuga(arg1, arg2, arg3)
    p arg1
    p arg2
    p arg3
  end
end

# [1] pry(main)> Hakozaru2.hoge
# 123
# "      ヒアドキュメントの内容\n"
# "arg3"
# => "arg3"

第2引数、第3引数はmodule_evalのドキュメントを見ると、動的に定義した第1引数の内容が、あたかも指定されたファイル(第2引数)の行数(第3引数)に書かれているかのように、スタックトレースなどで表現することができるというもの

module Hako1
  module_eval <<-RUBY
    def self.hoge
      raise
    end
  RUBY
end

# [1] pry(main)> Hako1.hoge
# RuntimeError: 
# from (eval):3:in `hoge'

module Hako2
  module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def self.hoge
      raise
    end
  RUBY
end

# [1] pry(main)> Hako2.hoge
# RuntimeError: 
# from sample_code.rb:135:in `hoge'

おおーちゃんと正しいファイル名と行数になってるー!(この例じゃ行数が正しいかはわからないかもですが)
ちなみに行数は + 1 しないと正しい行数にならないのと、 module_eval はローカル変数を外側のスコープと共有するので内側から name を参照することができている

1つ1つ分解すると全然普通にRubyの機能を使っているだけだけど、いくつか組み合わさると面食らいますね
あとヒアドキュメントの理解が足りてませんでしたすいませんと言う気持ち

exceptメソッド

お次は同じくactivesupportのcoreextのexceptメソッド

def except(*keys)
  slice(*self.keys - keys)
end unless method_defined?(:except)

メソッド定義も後置if/unlessってできるんですね(知らなかった…)

class Hakozaru
  def hoge
    p "first hoge"
  end

  def hoge
    p "second hoge"
  end
end

# hako = Hakozaru.new
# hako.hoge
# "second hoge"
# => "second hoge"

普通は後勝ち

class Hakozaru
  def hoge
    p "first hoge"
  end

  def hoge
    p "second hoge"
  end unless method_defined?(:hoge)
end

# hako = Hakozaru.new
# hako.hoge
# "first hoge"
# => "first hoge"

はぁーすげぇ〜
ちなみにこの method_defined?(:except) の判定が必要なのは、Ruby2.7系まではハッシュに except メソッドがなく、Ruby3.0から追加されているからである
(最初に判定入ったのはここ)

あと関係ないけど、もしクラスメソッドで判定したいならこんな感じ↓ですかね

class Hakozaru
  def self.hoge
    p "first hoge"
  end

  def self.hoge
    p "second hoge"
  end unless singleton_methods.include?(:hoge)
end

# [1] pry(main)> Hakozaru.hoge
# "first hoge"
# => "first hoge"

今後も見つけ次第ストックして見ていこう