おもしろwebサービス開発日記

Ruby や Rails を中心に、web技術について書いています

prependとaliasは混ぜるな危険

これはRubyKaigi 2025のLT用に出したプロポーザルの内容をブログエントリにしたものです。プロポーザルは落選したのでここに書くことで供養しておこうと思います。

導入

既存のメソッドの定義を再利用しつつ新しい振る舞いを追加する方法としてはModule#prependが一般的な手法ですが、Module#prependがなかったころ(Ruby < 2.0)はaliasを利用していました。深く考えずに昔の名残でaliasを利用する人がまだいるかもしれません。しかし、aliasModule#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

しかし、aliasModule#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-tenantattr-encrypted が共にActiveRecord::Base#reloadをオーバライドしており、かつattr-encryptedがalias形式を利用していたためGemfileの記載順がactiverecord-multi-tenant, attr-encryptedの順のときにSystemStackErrorになるケースが有りました( 今はこの問題を解消するPRがmergeされているため最新版を使えば問題ありません)。

まとめ

aliasModule#prependの組み合わせにより起きる不具合について紹介しました。

この不具合は基本的に複数のライブラリの組み合わせによって発生する、というのとライブラリの読み込み順番によっては起きない、というやっかいな性質を持っており、めったに踏まないとは思いますが踏んだときの調査は面倒です。2025年にメソッドの振る舞いを変更する方法としてaliasを使う理由は基本的にない*2と思うので、もしaliasを使っている箇所を見つけたらModule#prependに書き換えていきましょう。

*1:コードを見たい方はこの辺りを眺めると良いと思います ruby/vm_method.c at ce849d565bf6aae8e0179fffb04eb1f665f17347 · ruby/ruby

*2:例外としてはRuby2.7以下をサポートしているライブラリがKernelなどの組み込みモジュールのメソッドを上書きしているケースですが、もうRuby3.0がEOLの時代なのでそういったライブラリも数少ないはず…