sumimさんの「Ruby1.9のクラスのメタ階層を整理する」という記事、Rubyの型階層は雑然としているというのは、なんかsumimさんの図が悪いような気もするなぁ。整理すればもうちょっと情報を引き出せるよ。あと、モジュールのせいもある。
前提知識
Rubyは基本的には単一継承のクラスベースオブジェクト指向言語なのだけれども、幾つか注意すべき処がある。
- クラスはClassクラスのインスタンスである。
- モジュール
- 制限付きの実装多重継承をもたらす仕組み。内部的には、モジュールの「化身」となるクラスを継承階層を挟み込むことで実装されている。
- 以下、モジュール
Mに対してその化身クラスをI(M)と表記する。 - 詳しくは以前の記事を参照。
- 特異クラス
- 特定のオブジェクトに専属するクラスのこと。特定のオブジェクトにだけ存在するメソッド「特異メソッド」を定義すると、内部的には特異クラスを生成してそれをクラス階層に挟み込んで表現する。
- 以下、オブジェクト
objに対してその特異クラスを(obj)と書く。
なお、sumimさんの記事では特異クラスは#<Class:<obj>>のように表記されている。これは特異クラスのinspectが返す文字列表現に由来する表記だが、長いので私はRHG式表記を使う。
- クラスはモジュール
- クラスはモジュールの一種でもある。どうしてこういう設計になっているかは別記事参照。
- 隠蔽
- 化身クラスや特異クラスというのはRuby内部の実装の詳細の話だ。モジュールや特異メソッドを実現するためにそういうやり方をしているという話に過ぎない。だから、これらはRubyレベルには出てこないようになっている。
- ただし、特異クラスは隠蔽漏れがあって、
(class << obj; self end)というイディオムで取得できる。
詳しいことは以前の記事を参照。
拡張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で表してある。
いや、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の関係と同じ形になる。
結論
Rubyの階層はSmalltalkよりも雑然として見える。
- sumimさんの図が悪い。でも、それだけが原因じゃない。
- メタクラスがクラスでもあるせいで、早めに再帰的な矢印が戻ってきてごちゃごちゃする。
- モジュールという別のレイヤーのものを書き込まなければならないのでごちゃごちゃする。
- 化身クラスまわりを真面目に書くと、化身クラスはあちこちに発生するのでごちゃごちゃする。
- 三次元で、継承階層とmix-in階層を別の方向に分けて描くとたぶんすっきりする。
- Ruby 1.9が、メタクラスの特異クラスを
Classにしてしまったのは、クラスとメタクラスを区別すると言うことだ。クラスはオブジェクトとして振る舞って結構だがメタクラスをクラス扱いするな、ということだ。これはSmalltalkへの先祖返りか?
宣伝
……というような話を、もうちょっと実用的な範囲に限定しつつもうちょっと丁寧に説明したのが『初めてのRuby』の8章です。よろしく。
コメント