Ruby の“特異メソッド”と“モジュール”を Squeak Smalltalk で


スーパーマリオブラザーズの「次の面」を求める 〜Rangeとsuccメソッドの甘い(?)関係〜 - http://rubikitch.com/に移転しました と似たようなことを Squeak Smalltalk の UniClass と Trait でマネしてみる実験。


Ruby の「特異メソッド」こと“インスタンス特異的なメソッド”は、そのインスタンス特異的な無名で通常は不可視なクラスである「特異クラス」を作り、それにメソッドを定義することで実現されています。

Squeak Smalltalk では、#assureUniClass というメソッドがあって、これをオブジェクトにコールさせることで、そのオブジェクト特異的なクラス(ユニーククラス。UniClass)を作ることができます。このクラスに適宜メソッドを追加することで Ruby の特異メソッドと同じようなことが可能です。


また、Ruby の「モジュール」ことミックスイン機構は、“モジュール”と呼ばれるある種の抽象クラスをメソッドホルダに見立て、それを include することにより、そのクラスの継承パスに仮想的に差し込むことで、モジュール中に定義されたメソッドを複数のクラスから共有することを可能にしています。なお、オブジェクトをモジュールで extend するのは、そのオブジェクトの特異クラスに同じモジュールを include するのと同じです。

これに対して、Squeak Smalltalk には(抽象)クラスとは別に“トレイト”と呼ばれるメソッドホルダがあり、これをクラスから use することで、トレイトで定義されたメソッドを複数のクラスから共有できます。Ruby のモジュールとの大きな違いは、ターゲットクラスのメソッドの拡張に継承機構を必要としない(継承パスを“汚染”しない)ことと、メタクラスRuby でいうところのクラスの特異クラス)も同時に拡張できる点です。


さて、くだんの「マリオのレベル」では、'1-1' タイプな文字列に対し、#succ をうまく機能させることができるよう MarioLevelSucc モジュールで extend しています。Squeak Smalltalk でも、似たようなメソッドのオーバーライドをする MarioLevel なるトレイトを定義し、'1-1' タイプな文字列特異的に作ったクラスに use させればよいはずです。

ただ残念ながら Smalltalk には Ruby の #succ に相当する概念がありません。そこで、#+ および #- を定義する必要があります。ここでは簡単のため、「+ anInteger」と「- aLevel」というメッセージに応答し、それぞれ、「足した数に見合った新しいレベル」と「各レベルの差の数」を返すことを想定することにしました。

Trait named: #MarioLevel
    uses: {}
MarioLevel >> + anInteger
    ^self class newFrom: self value + anInteger
MarioLevel >> - other
    ^self value - other value
MarioLevel >> value
    ^{self last digitValue. self first digitValue} - 1 polynomialEval: 4
MarioLevel classTrait >> newFrom: anInteger
    ^('{1}-{2}' format: {anInteger // 4. anInteger \\ 4} + 1) asLevel


このように定義した MarioLevel を '1-1' なる文字列の特異的なクラスに use させる(この場合には #addToComposition: を MarioLevel を引数として添えてコールする)ことで、正しい「次の面」を生成することが可能になります。(追記:あー、おもいっきり嘘つきました(^_^;)。上の #newFrom: でもう asLevel しちゃっているので、あとで出てくる String>>#asLevel はすでにこの時点で定義しておかないといけません。ごめんなさい。)

| level |
level := '1-1'.
level assureUniClass class addToComposition: MarioLevel.
^level + 4   "=> '2-1' "


もちろん、いちいち「assureUniClass class addToComposition: MarioLevel」するのは面倒なので、String>>#asLevel を定義してこれをコールさせましょう。あと、MarioLevel を use したクラスのインスタンスではたんに self を返すよう、MarioLevel>>#asLevel も同時にオーバーライドしておきます。

String >> asLevel
    self assureUniClass class addToComposition: MarioLevel.
    ^self
MarioLevel >> asLevel
    ^self
| level |
level := '2-2' asLevel.
^level + 4   "=> '3-2' "


Smalltalk にも Ruby の Range にあたる、Interval があります。コンストラクタは Interval class>>#from:to: ですが、通常は、「始点 to: 終点」という式で生成するので #to: を MarioLevel にも定義しておきます。

MarioLevel >> to: other
    ^Interval from: self to: other


ふう。#succ と #level の定義だけで済む Ruby よりかなり見劣りがしてしまいますが、とりあえずこれで当初の目標は達成です。

('1-1' asLevel to: '3-4' asLevel) asArray
=> #('1-1' '1-2' '1-3' '1-4' '2-1' '2-2' '2-3' '2-4' '3-1' '3-2' '3-3' '3-4')


もちろん言うまでもなくここで書いたことは“お遊び”ですので、よい子は真似をしないように。w そもそもこの場合、インスタンス特異的メソッドである必要がない(インスタンスはどれも同じ挙動をする)ので、Ruby はともかく Smalltalk では、きちんとそれなりのクラスを定義すべきでしょう。念のため。


追記

ネタ元のページで、裏面にも対応するアップデートがあったので当方も。#newFrom: の定義にすこし手を加えて。

MarioLevel classTrait >> newFrom: anInteger
    ^('{1}-{2}' format: {anInteger // 4 + 1 radix: 16. anInteger \\ 4 + 1}) asLevel
('8-1' asLevel to: 'D-4' asLevel) asArray
=>  #('8-1' '8-2' '8-3' '8-4' '9-1' '9-2' '9-3' '9-4' 'A-1' 'A-2' 'A-3' 'A-4'
      'B-1' 'B-2' 'B-3' 'B-4' 'C-1' 'C-2' 'C-3' 'C-4' 'D-1' 'D-2' 'D-3' 'D-4')