Ruby の特異クラスについて(補足)

This entry was posted by on Saturday, 12 January, 2008

昨日のやつですが、やっぱり誤りがありました。エラソーなことを言ってましたが特異クラスは詳しくないし大して活用したことがなかったので、抜けがあった。

そのまえに、特異クラスについて記述にちょっと誤解をまねく表現がまずあります。 Ruby には特異クラスのための構文というのがあるのです。

s = "some string" class << s def is_some_string? true end end

は、

s = "some string" def s.is_some_string? true end

と同じです。この「class << s」から「end」までを特異クラス(の宣言)といいます。でもこの話は昨日のやつには混ぜると話がややこしくなりそうだったので省略しました。実際、前者はいっぱいメソッドを定義するときに「s.」がいちいち必要ない、といった程度のことだと思ってて、クラスっぽい構文でも書けるよ、という程度の認識だったんですね。それにまあ、特異クラス構文になったところで、たとえば String クラスのオブジェクトを String という識別子で取り出せるように特異クラスをRubyレベルで指定することはできないことには変わりはない、と気軽に考えていました。

でも、べつに前者は単なる構文糖ではないので(というか後者は前者に書き直せるが前者からは後者に直せない表現が存在する)、その不一致を突けば特異クラスは取り出せるじゃん、と気付きました。そういえばそうだった。 Yugui さんご指摘ありがとうございます

$s = "abc" $singleton = nil class << $s def f1(); true; end def f2(); false; end $singleton = self end

こんなスクリプトを load しまして、

irb(main):001:0> $singleton => #<Class:#<String:0x53a9f8>> irb(main):002:0> $s.is_a?($singleton) => true irb(main):003:0> $s.class == $singleton => false irb(main):004:0> $singleton == String => false irb(main):005:0> $singleton.superclass => String irb(main):006:0> $singleton.ancestors => [String, Comparable, Object, Kernel, BasicObject] irb(main):007:0> String.ancestors => [String, Comparable, Object, Kernel, BasicObject] irb(main):008:0> $singleton.instance_methods => [:f1, :f2, ...]

こんな風に「f1とf2が定義されている」「$sは$singletonのインスタンス」「でも$sのクラスではない」「基底クラスはString」「ancestorsに自分がいない」という愉快なクラスが取り出せます。こいつは特異クラスです。

というわけでした。

One Response to “Ruby の特異クラスについて(補足)”

  1. sumim

    お邪魔します。昨日のエントリーではご紹介ありがとうございます。特異クラスについては Ruby ではクラスの特異クラスをメタクラスに使っていることもありますからさらにやっかいですよね(^_^;)。http://d.hatena.ne.jp/sumim/20080111/p1 にまとめてみましたので、よかったら参考にしてください。

Ruby のクラスとインスタンスの関係(あるいはモジュールと特異メソッドについて)

This entry was posted by on Saturday, 12 January, 2008
>
  • http://d.hatena.ne.jp/m-hiyama/20080109/1199863428 >
  • http://d.hatena.ne.jp/sumim/20080109/p2
  • うーん、タイトルでスルーしてたけどさすが面白い。誰か Rubyist が既に書いているかな?という気がしますけど。まあいいか。以下は少しだけ調べながら書きましたが、カンチガイなどにより嘘や不正確な情報が混じっている可能性があります。

    まず Ruby は Smalltalk に影響を受けた(とされる)オブジェクトシステムを使っています。ということで、 Object という識別子はクラスという概念を指すと同時に「Objectクラスオブジェクト」も指します。ややこしいことに Ruby にも Object.class という表現は存在するのですが、それは「Objectクラスオブジェクト」ではありません。「Object クラスオブジェクト」に対して class メソッドを呼び出しているので、「Object クラスオブジェクト」のクラス、つまり Class が返されます。

    また、 Ruby は実にミもフタもない構造を取っています。

    >

    どうでしょう、納得できましたか? 僕は納得できないですね(オイオイ)。Class.classというクラスオブジェクトの扱いが不徹底で気持ち悪いんです。 >

    クラスオブジェクトClass.classは、クラス・プレーンにあるクラスClassのレイフィケーション・イメージでした。が、Classというクラスがクラス・プレーンに在るのはおかしいのですよ。

    それを「おかしくない」と強弁してしまう(笑)のが Ruby です。 Class オブジェクトのクラスは Class 自身になっています。違和感の残る人もいるかもしれませんが、図としてはすっきりしている……と、ぼくは思います。

    ところで、 Ruby では Object クラスの基底クラスって何でしょうか? じつは 1.9.0 からは BasicObject という新しいオブジェクトができました。 Object じたいはけっこう多機能なので、そういう余計なものを削ぎ落とした「空っぽにちかいオブジェクト」が BasicObject です。また BasicObject の基底クラスは定義されません(BasicOjbect.superclass は nil が返されます)。ということで、以上をまとめて図に書くと、次のような感じ。なお、 1.8.x を使ってる常識的な皆さんには BasicObject は存在しないのでその辺を削除してください。

    さて、 Ruby で面白いのは「モジュール(Module)」です。 Ruby のモジュールは、乱暴な言い方をすると Java のインタフェースのようなものです(詳しい説明は逆引きRubyとかを読んでください……)。 Java ではインタフェースを実装する(implements)という宣言がありますが、 Ruby にも include という似たような表現があります。

    class String include Comparable end

    こんな風に書くと、 String のインスタンスではモジュールの提供するメソッドが使えたりします。

    面白いのは Ruby では個々のモジュールもまたオブジェクトだということです。つまり上で書いた Comparable というのは「モジュールオブジェクト」なのです。そしてモジュールオブジェクトは「Moduleクラス」のインスタンスとなっています。誤解しないでほしいのですが「Moduleクラスオブジェクト」じたいはモジュールではありません。あくまでも「Module」は「モジュールオブジェクトのためのテンプレート」となるクラスだからです。

    ところでモジュールはクラスとすごくよく似た概念で、メソッドなんかがふつうに定義できます。実際、 Ruby レベルではモジュールとクラスではほとんど同じことができます。ただしモジュールはインスタンスを作ることができず、上のように特定のクラスに差し込まれることで定義したメソッドを使うのが普通です。ようするにモジュールとは「クラスからインスタンスを作る機能を取り除いたもの」だと考えることができます。

    そこで、 Ruby では発想を逆にして「ClassはModuleを拡張してインスタンスを作る機能を付け加えたものだ」と考えます。すると、 Class は Module を派生させてできたものだ、と考えられそうですね。実際、 Ruby ではそうなっています。ちょっとややこしいですが、

    >
  • Class クラスオブジェクトは Class クラスのインスタンス >
  • Module クラスオブジェクトも、ほかのクラスオブジェクトと同じく Class クラスのインスタンス >
  • Class クラスの基底クラスはModuleクラス
  • ……ということです。また(おそらく便宜上)、 Module クラスの基底クラスは Object とされています。図にかくとこんな感じでしょうか。

    さあこんがらがってきました。なお、個々のモジュールは(クラスではないので)基底クラスという概念がありません。したがって基底クラスの矢印もありません。

    さて、「Stringクラスに Comparable モジュールを差し込む」というのはどういうことでしょうか。これは「モジュールオブジェクトがクラス継承に割り込む」という意味に解釈できます。以上をまとめて図に描いてみると、こんな感じでしょうか。

    String のオブジェクトである s1 に f というメソッドを呼びたいとします。 s1 は String のインスタンスですから String のメソッドをまず調べますよね。なければその基底クラスに……という風に辿っていく。でも、 String から Object へ辿る途中にモジュールが割り込むので、その段階でモジュールにあるメソッドも探され、発見されたらそっちのメソッドが呼ばれます。ということで、「モジュールに定義されているメソッドが String でも使える」ということになります。

    また、モジュールそれ自体は Module とインスタンス関係しかないので、個々の s1 からすれば Module クラスのメソッドを呼ぶことはできません。あくまでも辿るのは継承関係だけ、ですよね。

    ただ、本当に Comparable がこの位置にくるわけじゃありません(そういうわけで点線で囲まれるノードになっています)。複数のクラスが同じモジュールを include したときに、 Comparable から先に辿るのが何物かわからなっちゃいますよね。たとえば Person もまた Comparable だとすると、次のようになります。

    このふたつの Comparable は実際には同じものです。2つあるように見えるのは、複数の継承関係に割り込んでいるからです。

    以上をまとめて Ruby スクリプトに書き起こしてみましょう。こうなります。よく見比べてください。

    require "test/unit" class Person attr_reader :name def initialize(name) @name = name end end class RubyClassModuleTest < Test::Unit::TestCase def test_classes_modules s1 = "Hello" s2 = "Bye-Bye" p = Person.new("tonkichi") " 1"; assert_equal(String.superclass, Object) " 2"; assert_equal(Person.superclass, Object) " 3"; assert_equal(Class.superclass, Module) " 4"; assert_equal(Module.superclass, Object) " 5"; assert_equal(Object.superclass, BasicObject) # ruby 1.8 ではエラー " 6"; assert_equal(s1.class, String) " 7"; assert_equal(s2.class, String) " 8"; assert_equal(p.class, Person) " 9"; assert_equal(String.class, Class) "10"; assert_equal(Person.class, Class) "11"; assert_equal(Object.class, Class) "12"; assert_equal(BasicObject.class, Class) "13"; assert_equal(Class.class, Class) "14"; assert_equal(Comparable.class, Module) "15"; assert_equal(Module.class, Class) end end

    なお、モジュールは superclass の関係にはあらわれてきませんが、 ancestors メソッドを使うと見ることができます。 ancestors はレシーバのクラスから基底クラスをずっと辿ってクラスオブジェクトの配列を返すメソッドですが、途中にモジュールが入るばあいはそのモジュールも配列に含まれるようになります。実際にはメソッド探索の順番を確認したいときに使われます。

    irb(main):001:0> String.ancestors => [String, Comparable, Object, Kernel, BasicObject]

    String の基底クラスは Object ですが、 String と Object のあいだに Comparable モジュールが差し込まれていますね。ちなみに、この結果はわたしが Ruby 1.9.0 を使っていることを示しており、 Ruby 1.8 系では次のようになります。

    irb(main):001:0> String.ancestors => [String, Enumerable, Comparable, Object, Kernel]

     

    さて、さらに話をややこしくしますと Ruby で特徴的なのは「特異メソッド」という怪しげな仕組みです。これをつかうと、特定のオブジェクトが個別にメソッドを持つことができます。次の例を見てください。

    irb(main):001:0> s1 = "some string" =>"some string" irb(main):002:0> def s1.is_some_string?; true; end => nil irb(main):003:0> s1.is_some_string? => true irb(main):004:0>"other string".is_some_string? NoMethodError: undefined method `is_some_string?' for "other string":String irb(main):005:0>"some string".is_some_string? NoMethodError: undefined method `is_some_string?' for "some string":String irb(main):006:0> s3 = s1 =>"some string" irb(main):007:0> s3.is_some_string? => true

    ここでは「s1という文字列オブジェクト」に is_some_string? というメソッドを定義しています。このメソッドをほかのオブジェクトである "other string" に対して呼び出すとエラーになります。また "some string" と内容が同じでも、実態が異なればやっぱりエラーになります。しかし実態が同じ s3 に対してはメソッドを呼び出すことができますね。

    これ、実際にはどうなっているかというと、新しく特異クラスという怪しげなクラスをつくり、そのクラスに is_some_string? メソッドを定義して、「s1のクラス」を String から特異クラスに差し