Ruby1.9 のクラスのメタ階層を整理する

[ruby-core:18437] Class as second-generation singleton class を読んだ当初は、特異クラスのクラスが Class ってことでええやん!と思ったのですが、改めて調べてみるとどうやら必ずしもすべての特異クラスが Class に属するわけではないようで(かつ、確認する過程で、id:sumim:20080111:p1 の間違いを見つけてしまったり、id:sumim:20061019:p1 の謎が解けたりもしたので)、この機会に表題の件についていったん図にして自分なりに理解を整理しておくことに。

関連:


Ruby で、クラスのメタ階層の情報を得る際の注意として、Ruby の #ancestors は隠しごとをするし(特異クラスは通常は不可視)、#class と #superclass はレシーバが特異クラスであっても正確な情報を返さないので、これらはほとんどあてにならないということを知っておく必要があります(#superclass に関しては、1.9 では建前と実際が一致するようになったみたいです)。あと、class << self; self end のイディオムは特異クラスがないときに作ってそれを返してくる副作用(というか本来の機能)があることも忘れてはいけないでしょう。


こうした制約の下で、Ruby のオブジェクト(主にクラスや特異クラス)が見かけ上ではなく本当に属していクラスを知るには evil-ruby に yugui さんのパッチを当てることで利用できる #actual_class、#actual_superclasss を使うのが良さそうです。ただ残念ながら、手元の環境では evil-ruby が 1.9 でうまく動かなかったので、1.8 で調べた結果をもとに、あとは手探り(適当なクラスへのメソッドの追加とそれを起動可能か否かのチェック)で 1.9 での違いがないか確認しつつ(yugui さんも書いておられるとおり両者に BasicObject 以外の違いはなかったわけですが…)まとめた結果が次の図です。

That information is old. The hierarchy was already changed. Thank sumim for the article. it helped us to decide the new hierarchy.

I’ll post an article describing the new hierarchy.

Yugui ― 26 September 2008

http://www.rubyflow.com/p/948


http://squab.no-ip.com/collab/uploads/61/ruby19meta.png
f:id:sumim:20211215122955p:plain


黒矢印が クラス←インスタンス関係を、赤矢印が継承パスを示しています。最初の一階層だけ黒い矢印を上り、あとはそこから連なる赤い矢印を連続してたぐることで、メソッドサーチを脳内でシミュレートできます。なお、グレイはそれが特異クラス(あとで示す Squeak Smalltalk ではメタクラス)であることを示しています

#actual_* を使って調べるまでは、#class 、#superclass の結果につられて Smalltalk とそっくりの構成で四層を成す…などと思い込んで書いてしまっていたのですが、実際にはずいぶんと違っていて BasicObject、Object、Module とそれ以外のクラスでは階層がシフトしたり同じ階層にクラスと特異クラス(メタクラス)が混在したりして、Smalltalk よりずっと雑然としていることが分かりました。


参考まで、Squeak Smalltalk ではこんな感じ。

http://squab.no-ip.com/collab/uploads/61/squeak39meta.png
f:id:sumim:20211215123232p:plain


ちなみに Ruby でも、特異クラスが #class で返す結果に従って描くと、見かけ上は階層ごとに整理された Smalltalk のそれとよく似た結果が得られます。

Ruby1.9 の場合

http://squab.no-ip.com/collab/uploads/61/ruby19metafake.png
f:id:sumim:20211215123250p:plain

Ruby1.8 の場合

http://squab.no-ip.com/collab/uploads/61/ruby18metafake.png
f:id:sumim:20211215123305p:plain

ただ、これには落とし穴があって、たとえば、Ruby1.8 の場合、Kernel のクラスである # は上流に # や Class を継承パスに含むように見えるため、Module のクラスメソッドや、Class に定義したメソッドをコールできるはずなのですが、実際にはそんなことはできずエラーになります。

class Object
  def singleton_class; class << self; self end end
end
Kernel.singleton_class                         #=> #<Class:Kernel>
Kernel.singleton_class.superclass              #=> #<Class:Module>
Kernel.singleton_class.superclass.superclass   #=> Class
def Module.m1; end
Kernel.m1   #=> NoMethodError
class Class; def m2; end end
Kernel.m2   #=> NoMethodError


ほかにも、特異クラスを持つ Foo のインスタンスが、見かけ上は # と # しか継承していないという現象もあったりして興味深いです。

class Foo; end; foo2 = Foo.new
foo2.singleton_class                         #=> #<Class:#<Foo:0x7ff6e0a8>>
foo2.singleton_class.superclass              #=> #<Class:Foo>
foo2.singleton_class.superclass.superclass   #=> #<Class:Class>


#class や #superclass が正確な情報を返さないのにはいろいろと事情があると思うので、それはそれとして他にも evil-ruby + yugui パッチのような機能が組み込みであると、Ruby の謎の動きの解明が簡単にできるのでよいのではないかなと思いました。