今度は Squeak と Haskell 数字について調べて行きます。数字は一番基本的でなじみやすいデータなので、言語の性質が現れやすいと思います。
数値型の構造
Squeak と Haskell それぞれで用意されている型は次の物があります。
これらの数値型は、Squeak では Number を頂点としたクラス階層によって実現されています。つまり、Float と SmallInteger の共通の機能は、Number や、さらにその基底クラスの Magnitude で実装されています。一方で、Haskell にクラス階層は無く、Int と Double の共通機能は共通の型クラスである Enum, Eq, Ord のデフォルト関数定義としてそれぞれ実装されます。
次に任意長整数について見てみましょう。Squeak では固定長整数 SmallInteger がオーバーフローを起こすと自動的に任意長整数を返します。例えば 2 raisedTo: 33 は 8589934592 を返します。一方で、Haskell では戻り値の型が決まっているので、(2 :: Int) ^ 33 は 0 になってしまいます。これは Squeak の動的型の利点が良く出ています。
一方で、Squeak では Complex を Number のインスタンスにできないという単一継承の欠点があります。これは、Number が Magnitude のサブクラス、つまり大小比較が必要という性質から来ています。Haskell では数値を表す Num と大小比較の Ord は独立した型クラスなので自然に実装出来ます。
また、Haskell の分数と複素数は総称型になっています。例えば (Integral a) => Ratio a というのは、分子と分母に型クラス Integral のインスタンス(この中では Int と Integer 型)が使える事を示します。Squeak でこのような総称型は不要ですが、Fraction の内部で不正な値が混ざらないように動的なチェックが必要です。
Squeak のダブルディスパッチ
数値を計算するとき、1.5 + 2 のように引き数の型が異なる場合があります。この際に Squeak ではダブルディスパッチという方法で型変換をします。例えば Float + Int の場合、Float の + の定義はこんな感じです。
+ aNumber <primitive: 41> ^ aNumber adaptToFloat: self andSend: #+
両辺の型が違うので primitive が失敗し、+ の右辺に adaptToFloat:andSend: が送られます。この場合、2 adaptToFloat: 1.5 andSend: #+ が実行されます。
adaptToFloat: rcvr andSend: selector
^ rcvr perform: selector with: self asFloat
adaptToFloat:andSend: の実装は Number にあります。この中で 1.5 perform: #+ 2 asFloat が実行され、1.5 + 2.0 になります。
Haskell のダブルディスパッチ
Haskell の場合、Squeak と同じような事をするのにいくつかのやり方があります。まず最初に思いつくのは Int -> Double -> Int のような型の関数を用意する事ですが、これだと型が固定になって + のような多態の関数に使えません。多態にするには型クラスを使いますが、Haskell98 では型変数が一つという制限があります。この動作を確かめるために簡単なプログラムを書いてみました。
data Point = Int :@ Int deriving (Eq, Show) instance Num Point where (x :@ y) + (x' :@ y') = (x + x') :@ (y + y') (x :@ y) * (x' :@ y') = (x * x'- y * y') :@ (x * y' + y * x') negate (x :@ y) = negate x :@ negate y abs (x :@ y) = abs x :@ abs y signum (x :@ y) = signum x :@ signum y fromInteger n = fromInteger n :@ fromInteger n
これは Squeak の Point クラスのように、数値演算子が使える座標です。次のように使います。Haskell では + だけ実装するというわけにはいかず、型クラスに必要な関数全てを一度に実装する必要があります。
*Main> (1 :@ 2) + (3 :@ 4) 4 :@ 6 *Main> (1 :@ 2) + 7 8 :@ 9
おや、不思議な事に、問題なく (1 :@ 2) + 7 が実行出来てしまいます。+ の型は a -> a -> a なので、あたかも勝手に右辺が Point に変換されてしまったかのようです。Haskell では暗黙の型変換は無いはずなのでこれはおかしい。実は、Haskell 98 Report によると、ソースコード中の整数リテラルは関数 fromInteger の適用を意味するそうです。例えば 7 という文字列があると、あたかも
fromInteger 7 :: 文脈中で期待される(推論された)型
のように振る舞います。つまり、リテラルに対してだけ暗黙の型変換がある事になります。このように Haskell のリテラルの型が周りの文脈から決まる一方で、Squeak では、ソースコード中の文字列 7 は必ず SmallInteger になるようにリテラルと型の対応は決まっています。
もう一つのダブルディスパッチは、Squeak と同じようにもう一つ別の関数を用意する方法です。Prelude の中では、リストの表示に使われています。show 関数でリストを表示する時、要素が文字の場合は "文字列" の形式で、その他のオブジェクトの場合は [1,2,3] の形式で表示する必要があります。つまり、リストそのものと要素の二つの型でディスパッチしている事になります。これを実現するために、リスト専用の表示関数 showList を用意して、さらに要素でディスパッチします。例えば show "hello" の場合、
show "hello" showsPrec 0 "Hello" "" showList "Hello" ""
のように簡約されていき、最後の showList で [Char] 専用の showList が実行されます。
これはどちらにせよトリッキーです。ghc 拡張ではもっとスマートなやり方がありそうですが、やった事無いから省略します。
まとめ
Squeak と Haskell の数値について比べてみました。Squeak では動的型を利用して大きな整数を簡単に使える利点があります。一方で、複素数を Number のサブクラスに出来ない等、単一継承の犠牲になっている部分もあります。特に数値に関する部分では Haskell の型クラスはすごい貢献をしていると思います。こんなに数をプログラムで自然に記述出来る方法は見た事無いです。
引き数ディスパッチについてはどっちもややこしいです。もしかしたらHaskell98 の型クラスの制限が無ければ、Haskell の方が自然に書けるかもしれません。
話は変わりますが、Smalltalk プログラマが Haskell で一番気に食わないのがメンバ名が他の関数名や他の型のメンバと被ってはいけない所だと思います。例えば一度 color というメンバを作ると、この名前を他の型で使ったり color という関数を作る事は出来ません。これではプログラムの設計の仕方が大幅に変わってしまいます。この辺 Haskell ではどうするべきなのかというのも興味がありますが、今日は単純な構造の数値だけを調べてみました。