言語ゲーム

とあるエンジニアが嘘ばかり書く日記

Twitter: @propella

Smalltalk のクラスと Haskell の型クラスを比較します。

私は Haskell の一番すごい所は型クラスだと思っているんですが、ここ数日それを言葉にしようとずっと悩んでいました。結局、もう一つの好きな言語なSmalltalk と比較して型クラスがどういう物か書いてみる事にしました。

型クラスという名前について

Smalltalk に染まってしまった人にとって、「型クラス」というのは最悪なネーミングです。あまりにも Smalltalk のクラスと違いすぎるからです。機能的には traits や Java のインタフェースから継承を除いた物と思ったら良いです。また、「継承」や「インスタンス」など、SmalltalkHaskell 両方で使われる言葉はことごとく別ものと思った方が良いです。

型クラスの位置づけ

Smalltalk をはじめとするオブジェクト指向言語の機能をそのまま Haskell で実現しようとすると最初はすごく戸惑います。日本語と英語で訳す言葉が見つからない時のように、そもそも前提にある文化が違うのです。ここは異文化体験だと思って真っ白な心で挑む事が肝心です。まずは Smalltalk の機能とHaskell を対応してみて、Haskell の中で型クラスがどういう位置にあるのか見てみます。

  • クラス継承: Smalltalk ではデータを継承出来ます。例えば、四角い形を継承してウインドウを作る事が出来ます。また、データの継承と同時にメソッドも継承され、こういうのを is-a 関係と呼びます。Haskell にこの機能はありません。逆に言えば、データの継承もオーバーライドも不要だ!という所が Haskell と型クラスの画期的な所だと思います。
  • カプセル化機能: Smalltalk は外部からインスタンス変数にアクセスする方法を持たず、すべてメソッドを経由する事でカプセル化を実現します。Haskell ではカプセル化は Module を使います。Module とはいわゆる名前空間の事で、公開する名前を決める事が出来ます。Haskell ではデータ構造への直接アクセスにはパターンマッチを使います。パターンマッチにはコンストラクタが必要なので、コンストラクタを非公開にすると内部も非公開になります。
  • 差分プログラミング: Smalltalk ではオブジェクト継承を使ってコードの重複を防ぎます。テンプレートパターンというやつです。Haskell では、似たコードを見つけるとそこを関数として抜き出して後で合成すると良いです。他に、型クラスにデフォルトの関数を定義してテンプレートパターンみたいに使う方法もあります。
  • 名前空間: Smalltalk では、しばしばクラスを名前空間として使います。例えば円周率は Number pi という名前で呼び出すので、円周率用のグローバル変数を用意しなくて良いです。Haskell では名前空間には Module を使います。
  • 多態: Smalltalk で継承でオーバーライドされたメソッドの他、継承関係に無くても同じ名前のメソッドを同じように呼び出す事で多態を実現します。しかし Haskell では、型クラスを使って「今からこの関数を多態で使うよ」と宣言します。だからたまたま同じ名前のメソッドというのは存在しません。宣言するのは関数名と型だけなので、実装はバラバラに書いても良いです。

Smalltalk のクラスと Haskell の型クラスを比較すると、Smalltalk がクラスに沢山の機能をまとめている事が分かります。型クラスとは、それらの機能のうちで多態に必要な物だけを取り出した物です。

Smalltalk の継承と Haskell の継承

Haskell でも「継承」という言葉が出てきますがニュアンスは随分 Smalltalk と違います。例えば Haskell では、数値を表す Num が 等値関係を表す Eq を継承します。しかしこれは Num の実装には Eq の実装が必要だという条件を表すもので、Smalltalk みたいに Eq の関数を Num の関数でオーバーライドするという事はありません。

一方で、Eq の == と /= のように片方が決まればもう一方も決まるような定義がある場合、型クラスにデフォルトの定義を書いておきインスタンス定義時にオーバーライド出来ます。このオーバーライドは一段階しかありませんが、Smalltalk の継承の使われ方に近いです。

このインスタンスという言葉も罠で、Smalltalk ではクラスから見たオブジェクトと呼びますが、Haskell では型クラス(インタフェース)から見たデータ型まはたコンストラクタ(実装クラス)をインスタンスと呼びます。

二つの仕組みで実装してみる。

簡単な例を使って両者を比較してみます。次の性質を持つ二通りの実装CartesianPoint と PolarPoint を作ります。それぞれデカルト座標極座標で座標を保持します。

  • Point : 平面上の点を表す (Squeak では GenericPoint)
    • +~ 二つの点を足し合わせる事が出来る。
    • 同値判定が出来る。
    • radian : X 軸との角度をラジアンで取り出せる。
    • degree : X 軸との角度を度数法で取り出せる。
    • coordinates : デカルト座標を指定してオブジェクトを作る。

さて、この通りに Squeak で作ってみます。まず、「抽象クラス」を GenericPoint という名前で作ります。

Object subclass: #GenericPoint
	instanceVariableNames: ''
	classVariableNames: ''
	poolDictionaries: ''
	category: 'PointExperiment'!

!GenericPoint methodsFor: 'accessing' stamp: 'tak 6/22/2009 10:52'!
coordinates: x with: y
	self subclassResponsibility! !

!GenericPoint methodsFor: 'accessing' stamp: 'tak 6/22/2009 10:54'!
degree
	^ 360 / (2 * Float pi) * self radian! !

!GenericPoint methodsFor: 'accessing' stamp: 'tak 6/22/2009 11:06'!
hash
	^(self x hash hashMultiply + self y hash) hashMultiply! !

!GenericPoint methodsFor: 'accessing' stamp: 'tak 6/22/2009 10:52'!
radian
	self subclassResponsibility! !

!GenericPoint methodsFor: 'accessing' stamp: 'tak 6/22/2009 10:52'!
x
	self subclassResponsibility! !

!GenericPoint methodsFor: 'accessing' stamp: 'tak 6/22/2009 10:52'!
y
	self subclassResponsibility! !

!GenericPoint methodsFor: 'accessing' stamp: 'tak 6/22/2009 11:14'!
+~ p
	^ self coordinates: self x + p x with: self y + p y! !

!GenericPoint methodsFor: 'accessing' stamp: 'tak 6/22/2009 11:04'!
= p 
	^ self x = p x and: [self y = p y]! !

これに対応する Haskell の型クラスはこんな感じ。Eq a => の所で Haskell の「継承」Point の実装は Eq も実装しなければならない事を示します。+~ と degree は型クラスにデフォルト定義を用意しておきます。a は実装する型を表す変数で、Haskell98 の規格では一つしか指定出来ません。つまり、Smalltalk と同じく単一ディスパッチですが、ghc は複数の型変数をサポートしています。

class Eq a => Point a where
    radian :: a -> Float
    coordinates :: Float -> Float -> a
    x :: a -> Float
    y :: a -> Float

    -- Minimal complete definition: radian, coordinates, x, and y
    (+~) :: Point b => a -> b -> a
    a +~ b = coordinates (x a + x b) (y a + y b)
    degree :: a -> Float
    degree p = 360 / (2 * pi) * radian p

次に、デカルト座標で座標を保持する CartesianPoint の Smalltalk 版です。

GenericPoint subclass: #CartesianPoint
	instanceVariableNames: 'x y'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'PointExperiment'!

!CartesianPoint methodsFor: 'accessing' stamp: 'tak 6/22/2009 11:13'!
coordinates: x0 with: y0
	^ self class new x: x0 y: y0! !

!CartesianPoint methodsFor: 'accessing' stamp: 'tak 6/22/2009 11:03'!
radian
	^ y arcTan: x! !

!CartesianPoint methodsFor: 'accessing' stamp: 'tak 6/22/2009 11:03'!
x
	^ x! !

!CartesianPoint methodsFor: 'accessing' stamp: 'tak 6/22/2009 11:04'!
y
	^ y! !


!CartesianPoint methodsFor: 'initialize' stamp: 'tak 6/22/2009 11:12'!
x: x0 y: y0
	x := x0.
	y := y0.! !

デカルト座標Haskell 版です。同値判定が居るので Eq を実装する必要があります。Eq の実装は deriving 出来るので冗長ですが、Haskell の継承が Smalltqlk の継承と異なる事を示すために書きました。このように、Point が Eq を継承していても、Eq の実装を Point のインスタンス定義の中には書きません。

data CartesianPoint =  Cartesian Float Float deriving Show

instance Eq CartesianPoint where
    Cartesian x y == Cartesian x' y' = x == x' && y == y'

instance Point CartesianPoint where
    coordinates x y = Cartesian x y
    x (Cartesian x' y') = x'
    y (Cartesian x' y') = y'
    radian (Cartesian x'  y') = atan2 y' x'

極座標Smalltalk

GenericPoint subclass: #PolarPoint
	instanceVariableNames: 'r theta'
	classVariableNames: ''
	poolDictionaries: ''
	category: 'PointExperiment'!

!PolarPoint methodsFor: 'accessing' stamp: 'tak 6/22/2009 14:09'!
coordinates: x with: y 
	^ self class new
		r: (x * x + (y * y)) sqrt
		theta: (y arcTan: x)! !

!PolarPoint methodsFor: 'accessing' stamp: 'tak 6/22/2009 11:20'!
radian
	^ theta! !

!PolarPoint methodsFor: 'accessing' stamp: 'tak 6/22/2009 11:20'!
x
	^ r * theta cos! !

!PolarPoint methodsFor: 'accessing' stamp: 'tak 6/22/2009 11:20'!
y
	^ r * theta sin! !


!PolarPoint methodsFor: 'initialize' stamp: 'tak 6/22/2009 11:18'!
r: distance theta: radian
	r := distance.
	theta := radian.! !

極座標Haskell 版です。deriving という機能によって Eq の実装を省略しています。Show や Eq のように標準で定義されたいくつかのクラスは実装を省略出来ます。

data PolarPoint =  Polar Float Float deriving (Show, Eq)

instance Point PolarPoint where
    coordinates x y = Polar (sqrt (x * x + y * y)) (atan2 y x)
    x (Polar r theta) = r * cos theta
    y (Polar r theta) = r * sin theta
    radian (Polar r theta) = theta

まとめ

同じインタフェースを持つデカルト座標極座標のオブジェクトを SmalltalkHaskell の両方で実装する事により比較してみました。Haskell では、Smalltalk の継承が持つ様々な機能を別の機能に振り分けて実現しています。特に多態の機能は型クラスが受け持っています。

Haskell の特徴は、Smalltalk のような継承機能、つまり、基底クラスのデータ構造を受け継ぎ、同じ名前のメソッドをサブクラスでオーバーライドするような機能を持たない事です。Smalltalk の継承は大変強力ですが、私はプログラムを不透明にする凶悪な機能だと思っています。今日作ったサンプルでは簡単すぎてまだ分かりませんが、もしも Haskell を参考にする事で継承を完全撲滅できたら良いなと思っています。

完全なソースを以下に置いておきます。 http://gist.github.com/134206