以下は why the lucky stiff による Seeing Metaclasses Clearly の翻訳です。
Rubyのメタプログラミングについて興味はあるけどまだきちんと理解していない人は、次の4つのメソッドをよく見てほしい。新たな発見があるかもしれない。
class Object
# 特異クラスはどこにでも隠れてる。
def metaclass; class << self; self; end; end
def meta_eval &blk; metaclass.instance_eval &blk; end
# メタクラスにメソッドを追加
def meta_def name, &blk
meta_eval { define_method name, &blk }
end
# クラスの中でインスタンスメソッドを定義
def class_def name, &blk
class_eval { define_method name, &blk }
end
end
僕はこれらのメソッドを metaid.rb というファイルに保存している。メタクラスの利用をサポートするちょっとしたライブラリだ。以下のメタクラスに関する解説を読んだ後も metaid.rb は便利なのでぜひ使ってほしい。それから、この記事のコードは実際に実行してみること。理解度が違ってくる。
クラスについて
さて、 Class というのは何か?それを知るために簡単なオブジェクトを1つ作ってみよう。
class MailTruck
attr_accessor :driver, :route
def initialize( driver, route )
@driver, @route = driver, route
end
end
m = MailTruck.new( "Harold", ['12 Corrigan Way', '23 Antler Ave'] )
#=> #<MailTruck:0x81cfb94 @route=["12 Corrigan Way", "23 Antler Ave"],
# @driver="Harold">
m.class
#=> MailTruck
オブジェクトには変数を格納できる。ここで言う変数とはインスタンス変数のことだ。 MailTruck オブジェクトが initialize されると 内部に変数 と @driver が生成される。さらに別の変数を持たせることもできる。@route
m.instance_variable_set( "@speed", 45 )
# => 45
m.driver
# => "Harold"
インスタンス変数 にはアクセサが与えられている。Ruby が @driverMailTruck のクラス定義部で attr_accessor :driver というコードに出会うと の読み取り・書き込みのためのメソッドが作成される。すなわちメソッド @driverdriver と driver= だ。
これらのメソッドはクラスに対して定義される。つまり、インスタンス変数はオブジェクト内に、アクセサメソッドはクラス内に保持されるわけだ。これら2つは全く別の場所なのだ。
ここで次のことをしっかり押さえておこう。
メソッドはオブジェクトではなくクラスによって保持される。
クラスもオブジェクト
クラスもオブジェクトであるという話は聞いたことがあるだろう。Ruby では「すべてがオブジェクト」であり、クラスもまた例外ではない。ならばクラスとオブジェクトは同じものなのか?
実際、オブジェクトに対して呼べるメソッドはクラスに対しても呼べる。その証拠にオブジェクトもクラスも ID がシンボルテーブル上に記載される。
m.object_id
# => 68058570
MailTruck.object_id
# => 68069450
しかし、既に述べたように「メソッドを保持するのはクラスである」という点で両者には大きな違いがある。ここで疑問を持つ読者がいるかもしれない。「クラスがオブジェクトであるとして、オブジェクトはそもそもクラスにより作られるはずだ。そうすると無限ループが生じるのではないか?」
結論から言うとそうはならない。クラスはオブジェクトとは実装が異なるからだ。Ruby のソースコードからの抜粋を下に示す。
struct RObject {
struct RBasic basic;
struct st_table *iv_tbl;
};
struct RClass {
struct RBasic basic;
struct st_table *iv_tbl;
struct st_table *m_tbl;
VALUE super;
};
よく見ると、クラスは m_tbl というメソッドを記録するためのシンボルテーブルと、スーパークラスへのポインタ super を保持している。この点でオブジェクトとは異なる。
しかし安心してほしい。Ruby プログラマにとってはクラスはやはりオブジェクトだ。なぜならいずれも「インスタンス変数を保持し Object クラスから派生している」という、オブジェクトと見なされるための基準を満たしているからだ。
o = Object.new
# => #<Object:0x815c45c>
o.class
# => Object
Class.superclass.superclass
# => Object
Object.class
# => Class
Object.superclass
# => nil
ちなみに、 Object クラスというのはシンボルテーブルのトップに位置する要素で、メソッドが他のどのクラスにも見つからなかったときだけ表に現れてくる。
では次にメタクラスとはいったい何なのかということについて考えてみよう。
メタクラスという語はそもそも「クラスを定義するクラス」を意味する。ただし Ruby ではこの定義がそのまま当てはまらない。というのは、Ruby で「クラスを定義する」のは(厳密にはクラスではなく) Class オブジェクトだからだ。
Class にメソッドを定義して、クラス定義の中でそれを呼び出せるか試してみよう。
class Class
def attr_abort( *args )
abort "Please no more attributes today."
end
end
class MyNewClass
attr_abort :id, :diagram, :telegram
end
上のコードは Please no more attributes today. と出力する。 attr_abort はクラスの定義部で呼び出し可能なのだ。
Ruby ではいつでもクラスを定義したり再定義したりできる。そこにいわゆる「メタ」な含みはない。クラスはメソッドを保持する、たったこれだけのことなのに、何が話を複雑にしているのか?
その要因は、先に示したように、Rubyではメタクラスの本来の定義が当てはまらないことにある。そこで個人的には Ruby のメタクラスというのは「自分自身を再定義するためにオブジェクトが利用するクラス」だと考えることにしている。
オブジェクトはメソッドを保持できない。実際、ほとんどのオブジェクトにはその必要もない。
しかし時にはオブジェクトにメソッドを持たせたいときがある。もちろん直接そうすることは出来ない。その代わりとして、Matz はメタクラスのメカニズムを作ってくれた。
メタクラスの利用例を一つ見てみよう。YAMLライブラリではオブジェクトが出力されるときに表示されるプロパティを自由にカスタマイズできる。
require 'yaml'
class << m
def to_yaml_properties
['@driver', '@route']
end
end
YAML::dump m
# --- !ruby/object:MailTruck
# driver: Harold
# route:
# - 12 Corrigan Way
# - 23 Antler Ave
上は、クラスのオブジェクト全部ではなく特定のオブジェクトにだけ出力スタイルの変更を加えたいときに便利だ。ここで変数 m に格納されたオブジェクトはプロパティを順番に出力する。しかし他の MailTrack オブジェクトは YAML ライブラリが適当に選んだ順番でプロパティを出力する。同様の方法は、 String クラス自体に変更を加えることなく一部の文字列だけを特定の形式で表示させたいような場合にも使えるだろう。
このように変数 m 内のオブジェクトだけに to_yaml_properties メソッドを与えた際、このメソッドはメタクラスに保持される。そしてメソッドを保持したメタクラスは継承チェインの中でオブジェクトの一つ上のレベルに位置づけられる。
ところで to_yaml_properties メソッドは次のようなシンタクスでも定義可能だ。
def m.to_yaml_properties
['@driver', '@route']
end
ここで、本記事の最初に示した metaid.rb のメソッド群をロードして次のように実行してみよう。
m.metaclass
=> #<Class:#<MailTruck:0x81cfb94>>
m.metaclass.class
=> Class
m.metaclass.superclass
=> #<Class:MailTruck>
m.metaclass.instance_methods
=> [..., "to_yaml_properties", ...]
m.singleton_methods
=> ["to_yaml_properties"]
class << m のようなシンタクスが意味するのは、メタクラスのオープンに他ならない。見えないところで Ruby がこの仮想的なクラスを呼び出してきてくれるのだ。 m.metaclass の実行結果に注意して欲しい。 #<Class:#<MailTruck:0x81cfb94>> と、オブジェクトの前にクラスが付加されている。
オブジェクトのメタクラス内に定義されたメソッドは、インスタンスメソッドではなく、シングルトンメソッドと呼ばれる。そのオブジェクトと一対一の関連を持つメタクラスの中で定義されるメソッドだからだ。
metaclass メソッドを使えばメタクラスを簡単に呼び出すことができる。通常はメタクラスを取り出すために class << self; self; end のようなコードを使うのだが、それよりずっとシンプルだ。せっかくなのでメタクラスにメタクラスはあるかどうか調べてみよう。
m.metaclass.metaclass
=> #<Class:#<Class:#<MailTruck:0x81cfb94>>>
m.metaclass.metaclass.metaclass
=> #<Class:#<Class:#<Class:#<MailTruck:0x81cfb94>>>>
このまま延々と続いて行きそうだ。しかしメタクラスのメタクラスは何かの役に立つのか?
結局メタクラスのメタクラスは普通のメタクラスと同じだ。普通のメタクラスはオブジェクトのメソッドを保持する。同様にメタクラスのメタクラスもメタクラスのメソッドを保持する。メタクラスもオブジェクトなのだから!
メタクラスのメタクラスに関して特別な点があるとすれば、あまり実用的な使い道がないというところだ。ほとんどの場合、メソッドが必要なのは継承チェインのそれほど深くないところにいる時だ。だれもクラス継承の森の奥深くで多くの時間を過ごしたくはないだろう。
m.meta_eval do
self.meta_eval do
self.meta_eval do
def ribbit; "*ribbit*"; end
end
end
end
m.metaclass.metaclass.metaclass.singleton_methods
=> ["class_def", "metaclass", "constants", "meta_def",
"attr_test", "nesting", "ribbit"]
メタクラスが実際に便利に使える場所はせいぜいクラスの階層を1つ上がったところだ。つまり、あるオブジェクトにメソッドを与えたいときだ。あるいは、後で見るように、特定のクラスにメタクラスを持たせたい場合もないではない。それ以外の場合、単に誰の目にも触れないメソッドをに保持させることくらいにしか使えない。まあ、そういう使い方がないとも言い切れない。
ここで一つ重要なことがある。メタクラスは継承チェインを延ばしていきはしない。オブジェクトのメタクラスを作成すると、そのメタクラスはオブジェクトの継承チェインに割り込んでくる。しかしメタクラスのメタクラスを作っても、それは元のオブジェクトに対して何の影響も持たない。
メタクラスとクラスの関係の素敵なトリック―メタプログラマ必携の書
もう一つの重要なポイントがある。非常に美味しい話だ。もしこの記事をここまで読んで残りを読まなかったとしたら意味がないとさえ言える。ここまででもオブジェクトとメタクラスについて少しは学べたはずだが、重要さはその比でない。
その前にもう一度、クラスの基本をおさらいしておこう。
- クラスはオブジェクトである。したがってインスタンス変数を保持できる。
- メタクラスはオブジェクトのシングルトンメソッドを保持する。それらは継承チェインに割り込んで、クラスのメソッドより先に呼び出されるようになる。
さて、ここに一つの問いがある。クラスの中でインスタンス変数を使ったことはあるだろうか?クラスメソッドからではなくて、クラス自体の定義中で。
class MailTruck
@trucks = []
def MailTruck.add( truck )
@trucks << truck
end
end
同じことはクラス変数を使ってもできる。
class MailTruck
@@trucks = []
def MailTruck.add( truck )
@@trucks << truck
end
end
上の2つのコードは全く同じ働きをする。実質的な効果は一緒だ。
通常は前者のクラス・インスタンス変数ではなく後者のクラス変数を使う方がいい。理由は2つある。
- クラス変数はクラスの変数であることがはっきりしている。
記号が2つ並んでいるし、混乱が少ない。@ - クラス変数は必要ならインスタンスメソッドからも参照できる。
次のコードは問題なく動く。
class MailTruck
@@trucks = []
def MailTruck.add( truck )
@@trucks << truck
end
def say_hi
puts "どうも、{@@trucks.length}台のうちの1台です!"
end
end
けれど次のコードは動かない。
class MailTruck
@trucks = []
def MailTruck.add( truck )
@trucks << truck
end
def say_hi
puts "どうも、#{@trucks.length}台のうちの1台です!"
end
end
ではクラス・インスタンス変数は何のためにあるんだろうか?無駄じゃないのか?!確かに上のような例ではクラス変数を使うに越したことはないわけで、そうとも言えなくはない。
しかし、上の例でもやはりメタクラスが関わっている。というのはクラスメソッドもメタクラスに保持されるからだ。そういう仕様になっている。
なので当然、 self を用いて書くことも可能だ。
class MailTruck
def self.add( truck )
@@trucks << truck
end
end
あるいはシングルトン・シンタクスを使ってもいい。
class MailTruck
class << self
def add( truck )
@@trucks << truck
end
end
end
クラス・インスタンス変数やメタクラス・インスタンスメソッドは、クラス自体の中では全くもって意味がない。けれども、そこに継承関係が関わってくると話は別だ。そしてここが重要なのだ。
class MailTruck
def self.company( name )
meta_def :company do; name; end
end
end
上のメソッドはきわめてシンプルだが、無限の可能性を秘めている。ここでは MailTruck クラスに新しい会社の名前を加えるクラスメソッドを追加して、 次のように MailTruck クラスを継承するクラスの定義部で呼び出せるようにしている。
class HappyTruck < MailTruck
company "Happy's -- We Bring the Mail, and That's It!"
end
実行してみよう。 company クラスメソッドが “Happy’s” という会社名とスローガンを引数にして実行されるはずだ。
ここで meta_def というメソッドはどんな働きをしているのだろうか?
ここに「メタ」の真価がある。 meta_def は company というメソッドを HappyTruck のメタクラスに付け加えてくれる。大事なのは、親クラス MailTruck のメタクラスではなく、派生クラスの HappyTruck メタクラスに加えてくれるということだ。
Dwemthy’s Array
これは一見すごく単純に見えるが、非常にパワフルなテクニックだ。シンプルなクラスメソッドを書くだけで派生クラスの定義部で使えるメソッドを加えることができる。実際この手法は Rails や Ruby/X11 やその他多くのシステムの実装に用いられている。『whyの(感動的な)Ruby 入門』で紹介したロールプレイングゲーム・プログラム Dwemthy’s Arrayもそのような例の一つだ。
自分自身 Dwemthy’s Array を制作するなかで色々な発見をした。その結果、 Creature クラスのコードは次のように簡潔なものになった。
class Creature
def self.traits( *arr )
return @traits if arr.empty?
attr_accessor *arr
arr.each do |trait|
meta_def trait do |val|
@traits ||= {}
@traits[trait] = val
end
end
class_def :initialize do
self.class.traits.each do |k,v|
instance_variable_set( "@#{k}", v )
end
end
end
end
meta_def と class_def は可読性が低くなりがちなメタプログラミング時のコードをスッキリとさせてくれる。 meta_def におけるクラス・インスタンス変数の使い方に注目してほしい。もちろん、この状況でクラス変数を使うことは出来ない。(コードを書き換えて試してみるといい。)
余裕があれば Dwemthy’s Array のページに書かれているとおりにモンスターを作成してゲームを発展させてみよう。かなり楽しめると思う。