Haskellのコードでしばしば見かける幽霊型について。
Phantom type(幽霊型)の特徴
- 左辺で定義した型変数が右辺に全く現れない型
- 具体的な値に興味はなく、型の情報さえあれば十分な時などに使われる
- 型の情報だけでやりたいことを実現するため、型レベルプログラミングの一種と捉えられる
Pantaom type(幽霊型)の例
例として、次のようなものを考えます。
- 時刻の「分」と「時間」を区別できる型を作りたい
- ただし、「分」・「時間」を表す値の型は同一の型としたい
- 時刻に関する計算の処理も同一のものとしたい
時刻を保持する型を定義
まず、時刻を保持する型を定義します。
-- 型変数unitは右辺に現れないので幽霊型と呼ぶ
newtype Time unit = Time Int
deriving (Num, Show)
ここで注目すべきは、型変数の'unit'が右辺で使用されていないことです。
このように、データ型定義の左辺のみに現れ、右辺には現れない型変数はphantom parameter(幽霊パラメータ)と呼ばれます。
時刻の単位を表す型の定義
さらに、時刻の単位を表す型を次のように定義する。
-- 空定義の型
data Minute
data Hour
上記の型は空の定義であり、具体的な値は持ちません。
具体的な値がない型なんて何の役に立つのだろうと思うかもしれませんが、Time 'unit'の'unit'の部分にこの型を当てはめることでタグのように使用することができます。
特定の時刻単位を持つ型の定義
上記により、特定の(温度)単位を持つ温度を表す型を定義することができます。
-- 空定義の型Hourがタグのように利用され、Time Minute型と区別することができる
oneDayInHour :: Time Hour
oneDayInHour = Time 24
-- 空定義の型Minuteがタグのように利用され、Time Hour型と区別することができる
oneHourInMinute :: Time Minute
oneHourInMinute = Time 60
Hour型とMinute型が'Time unit'の型変数である'unit'に相当しています。
上記の通り、Hour型とMinute型に対する具体的な値がなくても、「型」の情報だけがあれば'Time Hour'と'Time Minute'は異なる型として区別することができます。
異なる型同士の計算、例えば次の計算は型エラーとなるため、安全に使用することができます。
-- Time Hour型とTime Minute型は異なる型のため、エラーとなる
ghci> oneDayInHour - oneHourInMinute
• Couldn't match type ‘Minute’ with ‘Hour’
Expected type: Time Hour
Actual type: Time Minute
これで(簡単ではありますが)、当初の目標である次のことが達成できました。
- 時刻の「分」と「時間」を区別できる型を作りたい
- ただし、「分」・「時間」を表す値の型は同一の型としたい
- 時刻に関する計算の処理も同一のものとしたい
Time Hour型からTime Minute型への変換
Time Hour型からTime Minute型への変換も次のように容易にできます。
h2m :: Time Hour -> Time Minute
h2m (Time h) = Time (h*60)
これにより、次のような計算ができるようになります。
ghci> h2m oneDayInHour - oneHourInMinute
Time 1380
上記の実装が抱える課題
実は、上記の実装には課題が残っています。
Time unitの型変数である'unit'がとりうる型に対する制限が甘いことです。
この実装では、カインドがType(すなわち、零項の型コンストラクタ「*」)であるBool型なども型変数unitに当てはめることができてしまいます。
useless :: Time Bool
useless = 0
このような課題を解消するためには、次のような手法があります。
- GHC拡張のDataKindsを使用して型変数unitに適用できる型を制限する
- Timeをエクスポートせずに、スマートコンストラクタでTimeを生成する手法を使う
おさらい
改めて要点をまとめると、
- 幽霊型とは、
- 左辺で定義した型変数が右辺に全く現れない型
- 具体的な値に興味はなく、型の情報さえあれば十分な時などに使われる
- 型の情報だけでやりたいことを実現するため、型レベルプログラミングの一種と捉えられる
- 幽霊型パラメータに異なる型が当てはめられた場合、それらは互いに異なる型となる
- 安全に関数を使用するパターンとして活用することができる
- 意図せぬTime HourとTime Minute同士の計算の防止
- また、幽霊型パラメータは幽霊型の定義の右辺には現れないため、データ型の右辺や幽霊型を引数にとる関数の実装に影響を与えない
- 安全に関数を使用するパターンとして活用することができる
- 幽霊型パラメータに当てはめる型を制限するために、DataKindsやスマートコントラクタの手法を用いることができる
(参考資料)