Rubinius で循環継承

先日は Smalltalk で循環継承を起こして遊びました。

ProtoObject の謎と superclass - ¬¬日常日記

しかし CRuby では循環するような継承関係を作ることは出来ません(と思います、私がソースを読んだ限りでは無理だと結論しました)。CRuby においては継承関係は次の二通りの方法によって定められます。

  • クラスの作成時
  • モジュールの include 時

循環継承は結局のところ ClassA < ClassA のような形ですから、クラス作成時には親クラスを自分自身にすることが出来ないので無理です。またモジュールの include によっても仮の継承関係がつくられますが(と言っていいものなのかな?)、これも cyclic な関係にある場合に例外を出すようにチェックされているので無理です(class.c の rb_include_module をご覧下さい)。というわけで Ruby では squeak で出来たような循環継承を構成することが出来ません。もちろん循環継承は有害ですので出来なくて当たり前というか親切なんだと思います。

なぜ有害かと言えば、例えばメソッドの探索を行なう場合に循環継承を起こしていると探索が止まらない場合があります。いや、止めようと思えば止められると思いますが、わざわざそんなことにコストをかけても利点がちっともありません。というか、循環継承を起こすことに利点は全くないと思いますので(私の想像の範囲では思い付かないのですが、なんか使える場面とかあったりしますでしょうか)、色々面倒なことが起きないように禁止しておくのが一般的だろうと思います。循環しちゃったらタクソノミー的なクラス階層が作れないから脳味噌にもとっても厳しいと思います。

でもほら、禁止されれば禁止されるほど、やってみたいじゃないですか。そういう夢を Smalltalk というか squeak は叶えてくれたわけです。squeak で出来るのならば、じゃあそれに影響されている Rubinius でもきっと出来るわけでしょう。そう思ったのでやってみました。

class_a = Class.new
class_a.superclass = class_a

cls = class_a
while cls do
  p cls
  cls = cls.superclass
end

#<Class:0x9>
#<Class:0x9>
#<Class:0x9>
#<Class:0x9>
#<Class:0x9>
#<Class:0x9>
#<Class:0x9>
#<Class:0x9>
#<Class:0x9>
#<Class:0x9>
#<Class:0x9>
#<Class:0x9>
#<Class:0x9>
#<Class:0x9>
#<Class:0x9>
# ...

以上のように Rubinius ではこんなに簡単に循環継承を起こせます。だから次のように知らないメソッドを呼ぶと探し続けて止まらなくなります。

class_a = Class.new
class_a.superclass = class_a

class_a.new.asdf # 止まらないよ!

Rubinius は楽しいですね!

これだけで終わるととっても中身がないのでもう少し Rubinius の振舞を眺めてみることにします。例えばちゃんとモジュールの include で循環が起きないようにチェックしているかを確かめてみます(以下の例は CRuby では例外が発生します)。

module MA; include MA; end
class CA; include MA; end
CA.ancestors #=> [CA, MA, Object, Kernel]

モジュールが循環を起こして止まらないことを期待していたのですが、なぜかプログラムが終了しました。ではさらに次のような状況がどうなっているのかを確認してみます。

module MA; end
module MB; end
MA.include MB # Rubinius では include が private じゃない!
MB.include MA
class CA; include MA; end
CA.ancestors #=> [CA, MA, MB, Object, Kernel]

おお、なんということでしょう、これもまたきちんと停止してしまいました。こうした振舞いを見ると、ホントにこれで大丈夫なの?と疑問を持たざる得ません。では今度は include がちゃんとしているのか確かめてみます。

module MA; end
module MB; end

class CA; include MA; end
CA.ancestors
#=> [CA, MA, Object, Kernel]

MA.module_eval { include MB }
class CA; include MA; end
CA.ancestors
# => Rubinius だと [CA, MA, Object, Kernel]
# => CRuby(1.8系) だと [CA, MA, MB, Object, Kernel]

ほら、やっぱり不安的中。では Rubinius のソースを読みましょう。

kernel//module.rb の Module#include と Module#append_features がどうなっているのかを確認します。kernel/core/module.rb の include_cv は次のようになっています。

  def include_cv(*modules)
    modules.reverse_each do |mod|
      if !mod.kind_of?(Module) or mod.kind_of?(Class)
        raise TypeError, "wrong argument type #{mod.class} (expected Module)"
      end

      next if ancestors.include? mod

      mod.send(:append_features, self)
      mod.send(:included, self)
    end
  end

そんなこんなんで、大体のところ CRuby と同じことをしていることが分かりますが(詳細は kernel/bootstrap/module.rb, class.rb もあわせて読んでみて下さい)、困ったことに next if ancestors.include? mod なんてしちゃってます。これは CRuby とは明らかに異なる挙動です。CRuby では ancestors に mod が含まれていても、mod がインクルードしているモジュールについても再確認を行なっています。

http://i.loveruby.net/ja/rhg/book/class.html

そういうわけですから Rubinius は現状ではきちんと include を実装していないことが分かりました。つまり「module MA; include MA; end」は、MA.ancestors が include が呼ばれた時点で [MA] となっているので、「if ancestors.include? mod」が真となるために結果として無視されるわけですね。これなら確かに循環継承も起こさないのでプログラムが停止するわけです。

以上、循環継承にチャレンジすることで CRuby と Rubinius の相違を発見できました。こういうちょっと馬鹿げた試みから豆知識を得ることが出来てとてもお得な感じですね!
Rubinius のこの相違を修正するパッチもいずれ作成してみたいと思います。

っていうか、include について言えば CRuby にしたって、次のようになっててくれた方が嬉しいんじゃないのかと思います。

module MA; end
module MB; end

class CA; include MA; end
CA.ancestors
#=> [CA, MA, Object, Kernel]

MA.module_eval { include MB }
CA.ancestors
# => [CA, MA, MB, Object, Kernel]

昔、こういうのを期待してて、「なんでうまく行かないの?」と悩んだことがあります。でもきっとこうなってないのには理由があるんですよね。なにかな?