型クラスの現在のところの理解を Squeak Smalltalk で表現してみる


ここ数日で、Scala の implicit parameter がどうやって型クラスっぽいことを実現しているかというのが分かったような気になってきたのと、型クラスが「状態を扱わないメソッドの集合」であるということを聞いて「それってまさにトレイトそのもの(ただし Scala のそれではなくシェルリの)じゃないか!」とも思ったので、Squeak(あるいは Pharo)のトレイトを使って型クラスを表現してみるとどうなるか試して遊んでみました。もとより動的型の Smalltalk で何ができるわけでもないので、あくまで雰囲気だけですが。


以降に示す各式は Squeak の Workspace に適宜貼り付けて(一挙ではなく)順に評価( do it、あるいは結果が欲しければ print it )すると実行できます。

Pharo の Playground でもいちおう動作しますが、Pharo にはなぜか #uses: がないので、あらかじめ次式を評価して Squeak 同様の #uses: を追加しておく必要があります。

ClassDescription compile: 'uses: trait self setTraitComposition: trait asTraitComposition'


あと念のため、Smalltalk環境(IDEモドキ)の最小限の操作だけで遊べるように、クラスやトレイトの定義やそれらへのメソッドの追加はすべて式で、例えば

(トレイトの定義の式) compile: 'メソッドの定義'

というように表現しています。本来であれば、クラスブラウザなどの GUI ツールを用いるのがデフォ(このため GNU Smalltalk などの特殊な処理系を除き、Smalltalk にはクラスやメソッド定義のための構文が無い!)なのでこの点もどうぞあしからず。


まず型クラス Eq を模したトレイト TCEq として定義します。ちなみに Eq の定義は Haskell ではこうです。(Hugs.Prelude から抜粋。下の Ord も同じ)

class Eq a where
    (==), (/=) :: a -> a -> Bool

    -- Minimal complete definition: (==) or (/=)
    x == y      = not (x/=y)
    x /= y      = not (x==y)


この遊びでは既存のメソッドとかぶらないように EQ: NEQ: などとしました。

"type class Eq"
(Trait named: #TCEq uses: #() category: 'TypeClasses-Simulation')
compile: 'EQ: other
   ^(self NEQ: other) not';
compile: 'NEQ: other
   ^(self EQ: other) not'.


次に同じく型クラス Ord を模したトレイト TCOrd を定義します。Haskell の Ord の定義はこちら。

class (Eq a) => Ord a where
    compare                :: a -> a -> Ordering
    (<), (<=), (>=), (>)   :: a -> a -> Bool
    max, min               :: a -> a -> a

    -- Minimal complete definition: (&lt;=) or compare
    -- using compare can be more efficient for complex types
    compare x y | x==y      = EQ
		| x<=y      = LT
		| otherwise = GT

    x <= y                  = compare x y /= GT
    x <  y                  = compare x y == LT
    x >= y                  = compare x y /= LT
    x >  y                  = compare x y == GT

    max x y   | x <= y      = y
	      | otherwise   = x
    min x y   | x <= y      = x
	      | otherwise   = y


Ord は Eq を継承しているので TCOrd も TCEq を #uses: します。

"type class Eq => Ord"
(Trait named: #TCOrd uses: TCEq category: 'TypeClasses-Simulation')
compile: 'CMP: other
   ^true caseOf: {
      [self EQ: other] -> [#EQ].
      [self LE: other] -> [#LT]
   } otherwise: [#GT]';
compile: 'LE: other
   ^(self CMP: other) ~= #GT';
compile: 'LT: other
   ^(self CMP: other) == #LT';
compile: 'GE: other
   ^(self CMP: other) ~= #LT';
compile: 'GT: other
   ^(self CMP: other) == #GT';
compile: 'MAX: other
   ^(self LE: other) ifTrue: [other] ifFalse: [self]';
compile: 'MIN: other
   ^(self LE: other) ifTrue: [self] ifFalse: [other]'.


次にインスタンス Ord Int に相当するトレイト TOrdInteger を定義します。型クラスとそのインスタンスの関係からこれはクラスで表現したほうが相応しいかとも思ったのですが、後のことを簡単に済ませたいのでここはあえてトレイトにします。

型クラス Ord のインスタンスは #CMP: か #LE: を自前で用意する必要があると同時に、Ord は Eq のサブクラスでもあることから #EQ: か #NEQ: も同様に自前で用意する必要があります。ここではそれぞれ #LE: と #EQ: を定義しました。

"type class instance Ord Integer"
(Trait named: #TOrdInteger uses: TCOrd category: 'TypeClasses-Simulation')
compile: 'EQ: other
   ^self = other';
compile: 'LE: other
   ^self <= other'.


前述の“後のこと”こと、型クラスを機能させるための“マジック”については、ここでは簡単のためトレイトを #uses: することで代用しました。ちなみに Haskell だとここで暗黙の引数にメソッド辞書を、Scala なら辞書代わりのオブジェクトを渡すよう裏で細工が(ただしこれらの言語ではコンパイル時に)なされます。

Integer uses: TOrdInteger. "use magic"


では準備が整いましたので動作の確認をします。

3 EQ: 3. "=> true "
3 EQ: 4. "=> false "

3 NEQ: 3. "=> false "
3 NEQ: 4. "=> true "

3 CMP: 3. "=> #EQ "
3 CMP: 4. "=> #LT "
4 CMP: 3. "=> #GT "

3 LE: 3. "=> true "
3 LE: 4. "=> true "
4 LE: 3. "=> false "

3 LT: 3. "=> false "
3 LT: 4. "=> true "
4 LT: 3. "=> false "

3 GE: 3. "=> true "
3 GE: 4. "=> false "
4 GE: 3. "=> true "

3 GT: 3. "=> false "
3 GT: 4. "=> false "
4 GT: 3. "=> true "

3 MAX: 3. "=> 3 "
3 MAX: 4. "=> 4 "
4 MAX: 3. "=> 4 "

3 MIN: 3. "=> 3 "
3 MIN: 4. "=> 3 "
4 MIN: 3. "=> 3 "

うまく動いていますね。


ついでに #MEMBER: や #SORT も定義しようかと思ったのですが、当然のことながら Smalltalk でレシーバーに Eq a => [a] や Ord a => [a] といった縛りは設けられないためやむなく Array に定義するしかなく、ここまでそれっぽく動いていたのと比べるとかなり興ざめです。

Array
compile: 'MEMBER: y
   self ifEmpty: [^false].
   ^(self first EQ: y) or: [self allButFirst MEMBER: y]';
compile: 'SORT
   | pivot |
   self ifEmpty: [^self].
   pivot := self middle.
   ^(self select: [:x | x LT: pivot]), {pivot}, (self select: [:x | x GT: pivot])'.
#(1 2 3) MEMBER: 3. "=> true "
#(3 2 1) SORT. "=> #(1 2 3) "


次に Ord a => Ord [a] に対応するインスタンスを作りたかったのですがやはり無理なので Ord [Int] 相当で我慢します。トレイト OrdIntegerArray を作り同様のマジックを IntegerArray に使ってみましょう。

"type class instance Ord [Int]"
(Trait named: #TOrdIntegerArray uses: TCOrd category: 'TypeClasses-Simulation')
compile: 'EQ: other
   (self isEmpty and: [other isEmpty]) ifTrue: [^true].
   (self isEmpty or: [other isEmpty]) ifTrue: [^false].
   ^(self first EQ: other first) and: [self allButFirst EQ: other allButFirst]';
compile: 'LE: other
   self ifEmpty: [^true].
   other ifEmpty: [^false].
   ^(self first LT: other first) or: [
		(self first EQ: other first) and: [self allButFirst LE: other allButFirst]]'.
IntegerArray uses: TOrdIntegerArray. "use magic"


これで、IntegerArray 同士の等価や大小比較が可能になります。以下、動作確認です。

#(1 2 3) asIntegerArray EQ: #(1 2 3) asIntegerArray. "=> true "
#(1 2 3) asIntegerArray EQ: #(1 3 2) asIntegerArray. "=> false "

#(1 2 3) asIntegerArray NEQ: #(1 2 3) asIntegerArray. "=> false "
#(1 2 3) asIntegerArray NEQ: #(1 3 2) asIntegerArray. "=> true "

#(1 2 3) asIntegerArray CMP: #(1 2 3) asIntegerArray. "=> #EQ "
#(1 2 3) asIntegerArray CMP: #(1 3 2) asIntegerArray. "=> #LT "
#(1 3 2) asIntegerArray CMP: #(1 2 3) asIntegerArray. "=> #GT "

#(1 2 3) asIntegerArray LE: #(1 2 3) asIntegerArray. "=> true "
#(1 2 3) asIntegerArray LE: #(1 3 2) asIntegerArray. "=> true "
#(1 3 2) asIntegerArray LE: #(1 2 3) asIntegerArray. "=> false "

#(1 2 3) asIntegerArray LT: #(1 2 3) asIntegerArray. "=> false "
#(1 2 3) asIntegerArray LT: #(1 3 2) asIntegerArray. "=> true "
#(1 3 2) asIntegerArray LT: #(1 2 3) asIntegerArray. "=> false "

#(1 2 3) asIntegerArray GE: #(1 2 3) asIntegerArray. "=> true "
#(1 2 3) asIntegerArray GE: #(1 3 2) asIntegerArray. "=> false "
#(1 3 2) asIntegerArray GE: #(1 2 3) asIntegerArray. "=> true "

#(1 2 3) asIntegerArray GT: #(1 2 3) asIntegerArray. "=> false "
#(1 2 3) asIntegerArray GT: #(1 3 2) asIntegerArray. "=> false "
#(1 3 2) asIntegerArray GT: #(1 2 3) asIntegerArray. "=> true "

#(1 2 3) asIntegerArray MAX: #(1 2 3) asIntegerArray. "=> an IntegerArray(1 2 3) "
#(1 2 3) asIntegerArray MAX: #(1 3 2) asIntegerArray. "=> an IntegerArray(1 3 2) "
#(1 3 2) asIntegerArray MAX: #(1 2 3) asIntegerArray. "=> an IntegerArray(1 3 2) "

#(1 2 3) asIntegerArray MIN: #(1 2 3) asIntegerArray. "=> an IntegerArray(1 2 3) "
#(1 2 3) asIntegerArray MIN: #(1 3 2) asIntegerArray. "=> an IntegerArray(1 2 3) "
#(1 3 2) asIntegerArray MIN: #(1 2 3) asIntegerArray. "=> an IntegerArray(1 2 3) "


まったく面白みはないですが、いちおう #MEMBER:、#SORT も動きます。

{#(1 3 2) asIntegerArray. #(1 2 3) asIntegerArray. #(2 3 1) asIntegerArray} MEMBER: #(1 3 2) asIntegerArray. "=> true "
{#(1 3 2) asIntegerArray. #(1 2 3) asIntegerArray. #(2 3 1) asIntegerArray} SORT.
"=> {an IntegerArray(1 2 3) . an IntegerArray(1 3 2) . an IntegerArray(2 3 1)} "


型クラスの肝である“マジック”の部分、つまり既存の定義をいじらずに拡張できる細工をどう裏で実現するかはいろいろあっても良さそうです。また、エンティティとしての型クラスが、デフォルト実装を持てるインターフェイスや(Scalaのではなくシェルリの)トレイトが機能面で共有する類似性に個人的にはとても興味惹かれます。


参考: