これはRubyKaigi 2025のLT用に出したプロポーザルの内容をブログエントリにしたものです。プロポーザルは落選したのでここに書くことで供養しておこうと思います。
導入
既存のメソッドの定義を再利用しつつ新しい振る舞いを追加する方法としてはModule#prepend
が一般的な手法ですが、Module#prepend
がなかったころ(Ruby < 2.0)はalias
を利用していました。深く考えずに昔の名残でalias
を利用する人がまだいるかもしれません。しかし、alias
とModule#prepend
が組み合わされると思わぬ不具合に遭遇することがあります。
不具合の例
次のように、同じメソッドをModule#prepend
で上書きしてからaliasで上書きするとSystemStackErrorになります。
class Hello def say 'hello' end end Hello.prepend(Module.new do def say # (m) super end end) Hello.class_eval do alias orig_say say def say # (a) "#{orig_say} world" end end Hello.new.say #=> SystemStackError
しかし、alias
、Module#prepend
の順番ではエラーになりません。なぜでしょうか?
class Hello def say # (o) 'hello' end end Hello.class_eval do alias orig_say say def say # (a) "#{orig_say} world" end end Hello.prepend(Module.new do def say # (m) super end end) Hello.new.say #=> "hello world"
原因
これはaliasが「alias実行時点でのメソッド実装をそのまま利用して別名のメソッドを作成する」という仕様なため起きます*1。前者のコード例だとorig_say
はその時点でのHello#sayの継承ツリーで最初に見つかる(m)
を指すことになります。するとHello.new.say
は (m) -> (a) -> (m) -> (a) -> (m) => ... => SystemStackError
となります。しかし後者のコード例だとorig_say
は(o)
を指すため、Hello.new.say
は(m) -> (a) -> (o) => "hello world"
となり正常終了します。
今どきalias
を使ってメソッドに振る舞いを追加しているgemはそれほど多くありませんが、稀にあります。例えば以前activerecord-multi-tenant とattr-encrypted が共にActiveRecord::Base#reload
をオーバライドしており、かつattr-encryptedがalias形式を利用していたためGemfileの記載順がactiverecord-multi-tenant, attr-encryptedの順のときにSystemStackErrorになるケースが有りました( 今はこの問題を解消するPRがmergeされているため最新版を使えば問題ありません)。
まとめ
alias
とModule#prepend
の組み合わせにより起きる不具合について紹介しました。
この不具合は基本的に複数のライブラリの組み合わせによって発生する、というのとライブラリの読み込み順番によっては起きない、というやっかいな性質を持っており、めったに踏まないとは思いますが踏んだときの調査は面倒です。2025年にメソッドの振る舞いを変更する方法としてalias
を使う理由は基本的にない*2と思うので、もしalias
を使っている箇所を見つけたらModule#prepend
に書き換えていきましょう。
*1:コードを見たい方はこの辺りを眺めると良いと思います ruby/vm_method.c at ce849d565bf6aae8e0179fffb04eb1f665f17347 · ruby/ruby
*2:例外としてはRuby2.7以下をサポートしているライブラリがKernelなどの組み込みモジュールのメソッドを上書きしているケースですが、もうRuby3.0がEOLの時代なのでそういったライブラリも数少ないはず…