sumimさんの「 Ruby1.9のクラスのメタ階層を整理する 」という記事、Rubyの型階層は雑然としているというのは、なんかsumimさんの図が悪いような気もするなぁ。整理すればもうちょっと情報を引き出せるよ。あと、モジュールのせいもある。
前提知識
Rubyは基本的には単一継承のクラスベースオブジェクト指向言語なのだけれども、幾つか注意すべき処がある。
- クラスはClassクラスのインスタンスである。
モジュール
- 制限付きの実装多重継承をもたらす仕組み。内部的には、モジュールの「化身」となるクラスを継承階層を挟み込むことで実装されている。
- 以下、モジュール
M
に対してその化身クラスをI(M)
と表記する。 - 詳しくは 以前の記事 を参照。
特異クラス
- 特定のオブジェクトに専属するクラスのこと。特定のオブジェクトにだけ存在するメソッド「特異メソッド」を定義すると、内部的には特異クラスを生成してそれをクラス階層に挟み込んで表現する。
- 以下、オブジェクト
obj
に対してその特異クラスを(obj)
と書く。
なお、sumimさんの記事では特異クラスは #<Class:<obj>>
のように表記されている。これは特異クラスの inspect
が返す文字列表現に由来する表記だが、長いので私はRHG式表記を使う。
クラスはモジュール
- クラスはモジュールの一種でもある。どうしてこういう設計になっているかは 別記事 参照。
隠蔽
詳しいことは 以前の記事 を参照。
拡張evil-ruby
で、隠蔽された特異クラスや化身クラスを無理矢理取り出すために、前掲の記事のときに evil-ruby を拡張した。
拡張した分は本家にパッチ送ったんだけど反応がないし、そもそも開発されている気配がない。そこで、今回 githubにフォーク した。
対象ソースと解析
次のソースをRubyに読ませて、evil-rubyで解析してみた。
class Yapoo; end class MenseMidget < Yapoo; end rin = Yapoo.new kimiko = Yapoo.new anonymous_midget = MenseMidget.new class << anonymous_midget; end
sumimさんの図よりは、Rubyのクラス階層も幾分ましに見えるんでないか?
見れば分かるけれども、化身クラスは instance_of? Object
ではない。それ以前に、Rubyレベルでこのクラスをオブジェクトとして処理する可能性をRubyは考慮していない。だから、化身クラス周りでメソッド呼び出しをするとすぐにRubyが落ちる。どうしようもない部分はgdbで内部を覗いて補いつつなんとかして作ったのが上の図だ。
図の整理
この図をsumimさんの記事のSmalltalkの場合の階層図に合わせて配列してみるとこうなる。
随分とすっきりした。そして、Smalltalkでは4階層になっているところ、Rubyではクラスのクラスのクラスが再び Class
のクラスに戻ってくるので2階層になっていることが分かる。言い換えると、これはメタクラスはクラスの一種であるということだ。
モジュール
まあ、先ほどの図には抜けている部分があって、Kernelモジュールがない。Rubyの場合はモジュール機構があるから、この2階層の他にモジュールの階層ができるわけだ。
先ほどの図にモジュールを差し込んでみると次のようになる。化身クラスは書かずに include
で表してある。
=begin
複雑になったのは、モジュールをクラス継承と一緒の面に書くから悪いんだと思う。モジュール階層はモジュール階層で別個のレイヤーをなしていて、だから3次元に書くのが正しい。間の2行を上にごそっと持ち上げれば、綺麗になるはずでしょ?
ちなみに、sumimさんのSmalltalk図風にもうちょっと整理したらこうなった。
いや、ModuleクラスやClassクラスを、この下のほうの行に置いてしまうのは妥当か分からないのだけれども。sumimさんの図はSmalltalkにクラスとメタクラスの区別があるのを踏まえて、それで振り分けているように見える。Rubyではメタクラスは特異クラスの一種なのでクラスなのだが、一応、次のように分類した。
で、上から順にメタクラス、狭義のクラス、インスタンス、と並べてある。
思ったこと
(BasicObject)
と (Object)
のクラスが (Class)
ではなくて Class
だというのは謎だ。これが (Class)
になっていれば、ちょうどSmalltalkの4階層が、メタクラスをクラスにしたことで2階層に潰れた形になって綺麗なのだが。実際、こうなる。
def Class.foo p :foo end Class.foo #=> :foo metaBasicObject = (class << BasicObject; self end) metaBasicObject.ancestors #=> [Class, Module, Object, Kernel, BasicObject] metaBasicObject.foo #=> NoMethodError
(BasicObject)
は Class
のサブクラスなのに、 Class
のクラスメソッドを継承していない!
どうしてこうなっているかというと、たぶん、最初にクラス階層を初期化するときの順番の都合ではないかと予想される。直そうと思えば直せるけれども、普通メタクラスをそんなにいじくらないので誰も文句を言わなくて、なので実装の都合で放置されているんじゃないかと推測する。
モジュールの階層
ついでにモジュールの階層だけ別個に書いてみるとこうなる。次のソースを読み込んだと思いねぇ。
module Inteligence; end module Prayable include Inteligence end class Cattle; end class Pegasus include Inteligence end class Yapoo include Prayable end
Yapoo
の定義が最初と違うのは趣味に走ったら例がかぶっただけなので、気にしない方向で。
ちなみに、モジュールが Module
クラスの直接のインスタンスであるのは「初期状態」の話だ。
class << Inteligence; end
と、メタクラスに触ったり、
module Inteligence def self.conscious?(subject = self) # do something end end
と、クラスメソッドを定義したりすると特異クラス (Inteligence)
が湧いてきて、 Kernel
, (Kernel)
, Module
の関係と同じ形になる。
結論
- sumimさんの図が悪い。でも、それだけが原因じゃない。
- メタクラスがクラスでもあるせいで、早めに再帰的な矢印が戻ってきてごちゃごちゃする。
- モジュールという別のレイヤーのものを書き込まなければならないのでごちゃごちゃする。
- 化身クラスまわりを真面目に書くと、化身クラスはあちこちに発生するのでごちゃごちゃする。
- 三次元で、継承階層とmix-in階層を別の方向に分けて描くとたぶんすっきりする。
- Ruby 1.9が、 メタクラスの特異クラスを
Class
にしてしまった のは、クラスとメタクラスを区別すると言うことだ。クラスはオブジェクトとして振る舞って結構だがメタクラスをクラス扱いするな、ということだ。これはSmalltalkへの先祖返りか?
宣伝
……というような話を、もうちょっと実用的な範囲に限定しつつもうちょっと丁寧に説明したのが『 初めてのRuby 』の8章です。よろしく。