Rubyのメタクラス階層

この記事は、先日開催した 第3回 RHGの逆襲 のまとめみたいなものである。と、同時に『初めてのRuby(仮題)』の宣伝である。

クラス、その例外、そのトリック

Rubyはクラスベースのオブジェクト指向だから、メソッドの情報はクラスに属している。インスタンスメソッドを呼び出すときには、そのオブジェクトの属するクラスを調べて、そのクラスの持っているインスタンスメソッドの中から探す。

でも、Rubyの場合は例外がある。1つはモジュール。モジュールはクラスではないのに、でもインスタンスメソッドを提供している。1つは特異メソッド。特異メソッドは特定のインスタンスに直接所属するメソッドだ。

でも、ここには実装上のトリックがある。Rubyにとってはモジュールのインスタンスメソッドも特異メソッドも、等しくクラスに属するインスタンスメソッドなんだな。

モジュール

モジュールをクラスにincludeすると、Rubyは内部的にモジュールの「身代わり」となるクラスを作成する。RHGではこれを「化身クラス」と呼んでいるので、以下ではそれにならう。

化身クラスはメソッド表(名前からメソッド本体を参照するHash)をモジュールと共有している。だから、モジュールが提供するのと同じインスタンスメソッドを提供していると言える。

include が行われたタイミングで、Rubyはクラスの継承ツリーにこの化身クラスを挟み込む。

class Cattle; end

class Yapoo < Cattle

  include FlowerGlowable

end

みたいなケースを考えよう。今、モジュール FlowerGlowable の化身クラスを FlowerGlowable′ と書くことにする。

include するまでは Yapoo の親クラスは Cattle だ。 include すると、 Cattle の子は FlowerGlowable′FlowerGlowable′ の子が Yapoo という構造になる。この構造は、 Yapoo.ancestors みたいなのを実行すると見て取ることができる。

20080413-module.png

特異クラス

特定のオブジェクトだけ、他の同輩たちには無い特別なメソッドを持たせたいと思ったらどうするだろう。特異メソッドとは要するにそういうものだ。

C++だったら、特別なメソッドを持った子クラスを作ってSingletonパターンを適用するというやりかたが選択肢に挙がるだろうなと思う。Rubyの特異クラスとはそれ、そのものだ。

クラス Yapombインスタンス kayo に特異メソッドを定義すると、 Yapomb を継承した新しいクラス (kayo) を作る。そして、新しいクラスにインスタンスメソッドを定義する。これが特異メソッドの正体である。

それからRubyは、インスタンス kayo が所属するクラスを新しいクラス (kayo) に書き換える。そして、ここでいう新しいクラス、のことを特異クラスと呼ぶ。特異クラスを inspect するとRubyでは #<Class:#<Yapomb:0xXXXXXX @name="kayo">> みたいに表示されるけれども、ここではRHGの表記に倣って (kayo) のように表記する。

before

20080413-eigenclass-before.png

after

20080413-eigenclass-after.png

特異クラス地位向上運動

特異メソッドを実装するのに特異クラスを使う必要はないし、Matzは当初は他の案も検討していたらしいと聞いた。例えば、直接オブジェクトにメソッド表を持たせても良いわけだ。

つまり、特異クラスという存在はMatz's Ruby Implementationの実装の詳細であり、Rubyの公式な仕様ではない。RHGにはそう書いてある。

でもなー、特異クラスは非公式という前提は特異メソッド定義式を導入した時点で破綻していると思う。既に、私みたいな特異クラス好きの間ではclass << obj; self end として特異クラスを取り出すイディオムは常識なわけで、Railsでも随分使われている。そこで、 [ruby-dev:34191] みたいな提案も出てくるわけだ。

クラスメソッド

ついでに言えばクラスメソッドとは、クラスの、 Class オブジェクトとしての特異メソッドである。別の言い方をすれば Class クラスを継承した特異クラスのインスタンスメソッドである。

クラスの特異クラス、これはクラスのクラスだからメタクラスと呼んでも差し支えない。Ruby界の英語ではmetaclass, singleton class, eigenclassという述語が混乱しているけれども、私としては次のように提唱したい。

  • 任意のオブジェクトに対して、その特異クラスのことを英語でeigenclassと呼ぶ
  • Class オブジェクトのeigenclassのことをmetaclassとも呼ぶ

メタ、メタ、メタ

metaclassもまたオブジェクトであり、特に kind_of? Class である。では、metaclassのクラスは? つまり、metametaclassの正体は何者か。

この意味では 星さんの記事 は誤解を与える恐れがある。

class << obj; end

この式の意味するところは、

  • objの特異クラスの定義を開始する
  • objが特異クラスを持っていなければ(普通のクラスに直接属しているなら)、新たに特異クラスを作成する

だ。

従って、それまで独自の特異クラスを持っていなかったオブジェクトも特異クラスを持ってしまう。つまり、今オブジェクトが真に属しているクラスはこのイディオムでは取得できない。

ちなみに、真に属する、とか言ってるのは、 Object#classClass#superclass のようなメソッドは特異クラスや化身クラスをスキップしてその親クラスを返すせいだ。これらの特殊なクラスはRubyレベルでは原則として目に見えなくて、操作できないのだ。

evil-ruby

evil-ruby というgemがある。DLライブラリの力を借りてスーパークラスの書き換え、所属しているクラスの書き換えと言ったRubyが禁止している動作を無理矢理実現してしまう邪悪なgemだ。

これを使えば「真の」所属クラスも取得できるんじゃね? と、 RHGの逆襲 第3回 で言った。

でも、今調べたらその機能はなかったのでevil-rubyパッチを書いた。あとで本家に送っとく。次のメソッドを足した。

  • 「真の」所属クラスを取得する Object#actual_class
  • actual_class を使ってメタクラス階層を遡っていく Object#classification
  • 「真の」スーパークラスを取得する Module#actual_superclass
  • それを使って「真の」先祖を取得する Module#actual_ancestors

evil-rubyは1.9対応が完全ではないので、ついでに Class クラスの処だけ直しておいた。

メタ階層連鎖

修正版evil-rubyと次のスクリプトを使って階層を辿った。

require './evil'

class Cattle; end

class Yapoo < Cattle; end


[Yapoo, Class, Object].each do |start|

  puts "#{start.inspect}:"

  puts "\tancestors:"

  start.ancestors.each do |klass|

    print "\t\t#{klass.inspect}, meta's:"

    p klass.classification

  end

  puts "\tmeta's"

  start.classification.each do |klass|

    print "\t\t#{klass.inspect}, ancestors"

    p klass.actual_ancestors

  end

end

オブジェクト-クラス-メタクラス-メタメタクラスの階層を辿るとこうなる。これはRuby 1.8の場合だが、1.9の場合でも BasicObject が入るだけで基本的に変わらない。

20084013-metaclass.png

つまり、初期状態ではメタメタクラスというものは定義されていないんだな。メタクラスに対して特異メソッドを定義しようとしたとき、初めてメタメタクラスみたいなものが作成されて、その場合は星さんの記事の通り1.8と1.9で挙動が異なる。

宣伝

……というようなネタを交えつつRuby入門するための入門書『初めてのRuby(仮題)』を書いた。さすがに、入門書なのでここまで深く突っ込んではないけど、その分だけ解説も上記よりは少し丁寧なはず。

で、本はオライリー・ジャパンより動物本の1冊として6月に刊行される予定である。動物は未定みたいだけど、Rubyコミュニティから希望を出したら反映される余地ってあるのかな? 聞いてみよう。

それにしても、一昨日脱稿してようやく自分がいかに緊張していたか気がついた。初の書籍執筆だもん。正直、今になって「あー、こっちの解説のほうが良かった」とか言う点も出てきている。もう少し肩の力を抜いて書ければ良かったかな。

これらの改善点は可能なら初校段階で入れるし、「(ただでさえスケジュール圧してるので)無理」と言われたらこのサイトでフォローアップしていくつもり。