このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

Haskellの型クラスに関わるワークアラウンド

次の2つの記事で、Haskellの型クラス機構に、場合によっては不都合が生じることを指摘しました。

具体的には次のような問題があります。

  • Collects e ce のようなマルチパラメータの型クラスが素直には定義できない。
  • 整数を台集合とする掛け算モノイドと足し算モノイドの2つのモノイドを同時には定義できない。

これらに対するワークアラウンド(回避策)を示します。2番めの問題への対策は、トリックを駆使した挙句、本末転倒なことになっています(泣)。

内容:

  1. コレクション型の型クラスを定義する
  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 のようにします。

……
……

フエーッ、こんなん、僕の欲しいオーバーロードじゃない!*2

*1:Haskellでの標準的な方法は、型クラスNumを経由して足し算モノイドと掛け算モノイドを定義するものです。

*2:例えば、掛け算でも足し算でも通用する話をするとき、演算子記号を◇とかして、(◇) = (*) か (◇) = (+) かの区別は、「話の文脈」として与えるべきなんでしょう。ここらへんをキチンと分析するなら、インスティチューション理論を使うべきかと思います。