Primeクラスの動作を調べてみた

Ruby1.9のPrimeクラス(素数列クラス)は以下のような面白い特徴を持っています。

  • 素数列」は複数存在しないのでSingletonである。
  • クラス自体が唯一のインスタンスと同じように振舞う。
  • 旧Prime互換クラスのインスタンスを作成できる。

以下で個別に解説していきます。
ソースはhttp://svn.ruby-lang.org/repos/ruby/tags/v1_9_1_preview2/lib/prime.rbを参考にして、解説に不要な部分をバッサリ省いています。
また、ruby-dev 35863から始まる一連の流れを動作を把握する参考にさせていただきました。

素数列」は複数存在しないのでSingletonである。

これは単純にSingletonパターンで実装されています。

class Prime
  @the_instance = Prime.new

  class << self
    def instance; @the_instance end
  end
end

Singletonモジュールを使用していない理由は、多分後述する旧Prime互換インスタンスを作成するためではないかと思っています。

クラス自体が唯一のインスタンスと同じように振舞う。

これは、全てのクラスメソッドを唯一のインスタンスに対してフォワードする形で実装されています。

class Prime
  @the_instance = Prime.new

  class << self
    extend Forwardable

    def instance; @the_instance end

    def method_added(method) # :nodoc:
      (class<<self;self;end).def_delegator :instance, method
    end
  end
end

Forwardable#def_delegatorは、第二引数で渡された名前のメソッドを、第一引数で渡されたオブジェクト(へのアクセサ)に対しての委譲としてselfに定義するメソッドです。extendで拡張しているので、呼び出しの際に特異クラスを呼んでいます。
この場合のselfはPrimeクラスの特異クラスなので、いわゆるクラスメソッドとして定義されます。

旧Prime互換クラスのインスタンスを作成できる。

これは、new(initialize)により個別のインスタンスを作成された際、そのインスタンスに互換用モジュールをextendすることで実装されています。

class Prime
  def initialize
    extend OldCompatibility
    warn "Prime::new is obsolete. use Prime::instance or class methods of Prime."
  end

  module OldCompatibility
# 旧Primeクラス互換インスタンスメソッド
  end
end

これにより、新Primeを使っている際に余計なメソッドを定義せず、互換が不要になればモジュールを削除するだけで対応できるようになっています。

感想

Primeクラスはインスタンスが生成でき、さらにクラス自体がまるでインスタンスのように使用できます。
モジュールならばmodule_functionで似たようなことができるのですが(インスタンスは生成できませんが)、クラスの場合はどうすれば良いのだろうと不思議になって調べてみました。
method_addedによるフォワード、extendによる互換性の保持は非常にスマートで実にRubyらしいと思います。
黒魔術*1を使わなくてもこれだけのものが作れる実例を見せていただきました。Yuguiさん石塚さんすげえ。自分もこんなコードが組めるようになりたいものです。

追記

もう一つ重要なことに気づきました。
クラス定義開始からinitializeメソッドまでをすべて抜き出すと以下のようになっています。

class Prime
  include Enumerable
  @the_instance = Prime.new

  # obsolete. Use +Prime+::+instance+ or class methods of +Prime+.
  def initialize
    @generator = EratosthenesGenerator.new
    extend OldCompatibility
    warn "Prime::new is obsolete. use Prime::instance or class methods of Prime."
  end
# 中略
end

3行目で唯一のインスタンスを作成しているのですが、これはクラス定義の中で行っています。*2
このタイミングではinitializeメソッドはまだ定義されていません。なので、この際に作成されるインスタンスにはOldCompatibilityはextendされません。インスタンスの生成がこれより後のタイミングになってしまうと、唯一のインスタンスが旧Prime互換になってしまい意味がありません。
一見すると単純なクラスインスタンス変数の初期化ですが、ここに置かれていることにはちゃんと意味があるんですね。

*1:「(class<

*2:クラス定義の中でそのクラスのインスタンスが生成できるのも不思議な感じですが、先にオブジェクトを作っておかないとklass(「Rubyの呼び出し可能オブジェクトの比較 (3) - なんかklassの話 @ 2006年11月 @ ratio - rational - irrational @ IDM」参照)を設定することができないからだと思われます