次の2つの記事で、Haskellの型クラス機構に、場合によっては不都合が生じることを指摘しました。
具体的には次のような問題があります。
- Collects e ce のようなマルチパラメータの型クラスが素直には定義できない。
- 整数を台集合とする掛け算モノイドと足し算モノイドの2つのモノイドを同時には定義できない。
これらに対するワークアラウンド(回避策)を示します。2番めの問題への対策は、トリックを駆使した挙句、本末転倒なことになっています(泣)。
内容:
- コレクション型の型クラスを定義する
- 整数の集合上に2つのモノイド構造を定義する(本末転倒)
コレクション型の型クラスを定義する
お馴染みになったであろうコレクション型の型クラスを再掲します。
class Collects e ce where empty :: ce insert :: e -> ce -> ce member :: e -> ce -> Bool
ここで、eは要素の型、ceはコレクション(要素の集まり)の型です。
僕は、「要素の型とコレクションの型から一意にコレクション構造が決まるなんて前提がオカシイでしょ」と言ったわけですが、想定される反論も記しておきました。「オーバーロードは何故にかくも難しいのか:Haskellの成功と失敗」より:
想定される反論は以下のものです; 勝手な型e, ceに対してコレクション構造が決まるわけではない。eとceは無関係ではなく、どちらかを決めればもう一方も決まる。例えば、要素の型がInt、コレクションの型は[Int](Intのリスト)のように。
この反論のアイデアに沿って考えます。コレクションの型ceに対して、要素の型eが一意に決まるなら、ce→e という対応があるので、それをElemとします。すると、e = Elem ce という等式が成立します。
e = Elem ce であるなら、型クラスCollectsの型パラメータは2つではなくて、ceただ1つになります。つまり、次のようです。
class Collects ce where type e = Elem ce empty :: ce insert :: e -> ce -> ce member :: e -> ce -> Bool
このままでは構文的事情でコンパイラを通らないので、中間の型変数eを消去して次のように書き換えました。型インスタンスを2つ定義しています。
{-# LANGUAGE TypeFamilies, FlexibleInstances #-} import Data.Maybe class Collects ce where type Elem ce empty :: ce insert :: Elem ce -> ce -> ce member :: Elem ce -> ce -> Bool instance Collects [Int] where type Elem [Int] = Int empty = [] insert x y = if member x y then y else (x : y) member x y = any (== x) y instance Collects (Maybe Int) where type Elem (Maybe Int) = Int empty = Nothing insert x y = Just x member x y = if isJust y then x == fromJust y else False
先頭にあるゴニョゴニョは、Haskellの言語拡張を利用可能にするオマジナイです。type Elem ce が、コレクションの型ceをもらって要素の型を返すような対応(型から型への関数)Elemの存在を宣言しています。
前提への疑問は棚上げしてるし、「なんだよ、マルチパラメータじゃねーじゃん」と言われそうですが、とりあえずうまくいくようです。
整数の集合上に2つのモノイド構造を定義する(本末転倒)
モノイドの型クラスと型インスタンスを再掲します。
class Mon m where mon_op :: m -> m -> m mon_unit :: m instance Mon Int where mon_op = (*) mon_unit = 1
こうすると、足し算のモノイド(↓)を型インスタンスとして定義できなくなるのでした。
instance Mon Int where mon_op = (+) mon_unit = 0
うまくいかない理由は、モノイド構造(台集合+演算+定数)に名前を付けられないからです。トリックを使って無理やりに名前を付けることにします*1。
{-# LANGUAGE TypeFamilies, FlexibleInstances #-} class Mon m where type Carr m mon_op :: m -> Carr m -> Carr m -> Carr m mon_unit :: m -> Carr m data IntMulMon = IntMulMon instance Mon IntMulMon where type Carr IntMulMon = Int mon_op _ = (*) mon_unit _ = 1 data IntAddMon = IntAddMon instance Mon IntAddMon where type Carr IntAddMon = Int mon_op _ = (+) mon_unit _ = 0
CoqやStandard MLでモノイドを定義するときは、台集合(carrier set, underlying set)とモノイド構造は別物なので、モノイド構造→台集合 という対応を定義できます。圏論では忘却関手と呼ぶものです。
Haskellにおける型クラスMonへのパラメータmは台集合を意味するのですが、「イヤッ、mはモノイド構造の名前なのだ」と無理やり思い込むようにして、台集合は Carr m (the carrier of m)で取り出すことにします(そう思い込む)。
data IntMulMon = IntMulMon が名前の準備です。ダミーのデータ型で名前を作っているわけです。type Carr IntMulMon = Int は、「モノイド構造IntMulMonの台集合は集合Intだ」と解釈されます。別な名前を用意してあげれば、別なモノイド構造を定義できます。
実際、足し算を演算とするモノイド構造のためにIntAddMonという別な名前も準備しています。本来は構文的機構でサポートすべきことを、意味領域側に名前オブジェクトを作って代用していて、いかにもバッドノウハウ。
mon_opとmon_unitの引数が増えてしまってますが、第1引数はダミーです。mon_op 2 3 のように書いても、2 * 3 か 2 + 3 かの区別が付かないので、第1引数を使って mon_op IntAddMon 2 3 のようにします。
…… ……