書いた人: sunaot
この記事は「最近 Ruby を始めたばかりで言語仕様についてよく知らない」という初級者や、「一通り記法は知っていて Ruby でプログラミングはできるが、その仕組みはよくわからない」という中級者へ向けて書いています。
Ruby を始めるならどんな本を読めばいいの? とたびたび聞かれます。そんなときは決まって、「他の言語が使えるなら『はじめての Ruby』が鉄板で、あとは実際に使いながらリファレンスマニュアルをなるべく参照するといいですよ」と答えます。
そうして実際に自分で Ruby のコードを読み書きし始めた人が、決まって「あれ?」と立ち止まるところがあります。今回はその中の一つ class << self の話です1。
リファレンスマニュアルへの参照など、より深く学ぶためのリンクもありますので、記事を読んでその仕組みに興味をもった人が理解を深めるきっかけになってくれれば執筆者冥利につきます。
Ruby のコードを読んでいると、
class Foo
class << self
def hello
puts 'hello'
end
end
end
という表記に出会うことがあります。この class << self という謎めいた表記はいったい何なのでしょうか?
この class << self は特異クラスと言いまして、説明をすると大変長いものです。
まず、なにをしているかというと、これはクラスメソッドを定義するイディオムのひとつです。マニュアルにも記載があります。クラスメソッドの定義の特異クラス方式と書かれている箇所です。
Ruby でのクラスメソッドの定義の仕方には大きくわけて二つのやり方があります。一つは特異メソッド方式、もう一つが特異クラス方式です。
どちらも正しいクラスメソッドの定義の仕方ですが、特異メソッド方式では複数のクラスメソッドをまとめて定義したい場合に都度の self. を書くのが面倒なため、そのようなときは特異クラス方式がとられることが多いようです。まずはこれだけわかれば Ruby を使う上で困ることは少ないでしょう。
特異クラス方式についてだけ、少し説明を補足します。まずはコードの例を見てみましょう。
# 特異クラスによるクラスメソッドの定義例
class Foo
class << self
def a_class_method
end
def another_class_method
# 二つ定義しても self つけなくていいから便利!
end
end
end
リファレンスマニュアルでの特異クラスの説明くらいは読んでみてもいいかもしれません。
しかし、実はこれだけではわかったようでなにもわかっていません。特異クラスについて真剣に学ぶのであれば、初めての Ruby の著者でもある yugui さんの書いたRuby のメタクラス階層あたりを読むのがよさそうです。さらに理解を深めたいなら、「プログラミング Ruby (ピッケル本)」、「パーフェクト Ruby」、「メタプログラミング Ruby」あたりの解説を読んでみるといいでしょう。
かつては Rails が class << self な特異クラス方式の書き方を多用していたため、After Rails の gem には特異クラス方式が流行っていたという印象があったのですが、執筆時点最新の Rails (ver4 系) を読むと特異メソッド方式と extend によるクラスメソッドの読み込みへ大部分が変わっていました。おかげでコードの見通しが非常によくなっています。これは学ぶべき点が多いですね。
さて、日常の Ruby コードで class << self を利用するための説明はここまでで終わりです。ここからは、実際に特異クラスやメタクラスを実感するために順を追って動きを見てみましょう。
Ruby では (クラスではなく) オブジェクトに対して直接固有のメソッドを定義することができ、それを特異メソッドと呼んでいます。
オブジェクトへ特異メソッドを定義するには下記のようにします。
hello = 'hello'
def hello.say(count = 1)
count.times { print self }
end
hello.say 2 #=> hellohello
another_hello = 'hello'
another_hello.say 3 #=> NoMethodError: undefined method `say' for "hello":String
このとき、say メソッドは String クラスのオブジェクト ‘hello’ に対してのみ定義され、String クラスが拡張されたわけではありません。つまり、別の String クラスのオブジェクトへ another_hello.say(3) としても NoMethodError となります。
オブジェクトに特異メソッドを定義するにはもう一つ方法があり、オブジェクトの特異クラスをひきだして、直接特異メソッドを定義することができます。
hello = 'hello'
class << hello
def say_world
puts "#{self}, world"
end
end
hello.say_world #=> hello, world
一見クラス定義に近い見た目ですが、これも << hello というところで hello オブジェクトの特異クラスを引き出しており、あくまでオブジェクトに対しての特異メソッドの定義となっています。
さて、ここでクラス定義のときの特異クラスの利用へ戻ると、
class Foo
class << self
としています。クラス定義のコンテキストでの self とは、Class クラスのインスタンス Foo class です。class Foo とはクラスを定義するときの記法ですが、Ruby の中での理解としては、Class クラスのオブジェクトを生成し Foo というグローバルな定数へ代入しています。
つまり以下の二つはほぼ同義です。
クラス定義のシンタクス class を使って Bar クラスを定義する場合。
class Bar
def hello
puts 'hello'
end
end
bar = Bar.new
bar.hello #=> hello
Class クラスのインスタンスを生成して、定数 Bar へ束縛する場合。
Bar = Class.new do
def hello
puts 'hello'
end
end
bar = Bar.new
bar.hello #=> hello
ここまでくるともう少しです。
定数 Bar に入っている Class クラスのオブジェクトへ特異メソッドを定義してみましょう。特異メソッドの定義の仕方は def object.method_name という形式でした。Bar のオブジェクトに特異メソッドを定義するので、def Bar.method_name と定義することになります。
Bar = Class.new do
def hello
puts 'hello'
end
end
def Bar.bye
puts 'good bye'
end
Bar.new.hello #=> hello
Bar.bye #=> good bye
Bar.bye という呼出しができました。これはクラスメソッドの呼出しと同じですね。つまり、クラスメソッド Bar.bye というのは、Bar に入っている Class クラスのオブジェクトへの特異メソッドの定義として読むことができます。
これにオブジェクトの特異クラスの引き出しの記法 << をあわせて考えると、最初のイディオムがやろうとしていることがわかってきます。
クラスメソッドの定義は特異メソッド形式と特異クラス形式があったのでした。特異メソッド形式で定義した def Bar.bye を特異クラス形式へ書きかえてみます。
Bar = Class.new do
def hello
puts 'hello'
end
end
class << Bar
def bye
puts 'good bye'
end
end
Bar.new.hello #=> hello
Bar.bye #=> good bye
できました。せっかくなので、クラス定義をすべて class 記法での宣言の中に入れてみましょう。クラス定義の中で Bar に入ってるインスタンスを取得するには self を使うのでした。
class Bar
def hello
puts 'hello'
end
class << self
def bye
puts 'good bye'
end
end
end
Bar.new.hello #=> hello
Bar.bye #=> good bye
ここでようやく最初に読んだ形が出てきました。クラスメソッドのための記法があるわけではなく、特異メソッドという仕組みを使って巧みにクラスメソッドが実現されていることがわかりますね。
Ruby で特異クラスと特異メソッドの記法を使ってメタクラス2へアクセスし、クラスメソッドとして働くメソッド定義ができることを順に見てきました。ふだん何気なく使っているものや読んでいるコードの仕組みを調べるおもしろさが少しでも伝わったなら、この記事の目論みは成功です。Enjoy programming!! :)
日本 Ruby の会、Asakusa.rb 所属 (先日ひさしぶりに参加した)。Rubyist Magazine 編集者。記事公開時点では Perl の会社ですっかり Ruby の仕事が多くなってきたプログラマ。sunaot@github | sunaot@twitter |