幽霊型とは
Phantom Type(和:幽霊型)
型パラメータを使用してコンパイル時には影響を与えるが、実行時の挙動には影響を与えない型を用いたデザインパターンの一種です。
コンパイル時にはいるが実行時にはいない、まるで幽霊。
定義
Haskellだと
data Phantom x a = Phantom a
data PhantomInt x = Phantom Int
Scalaだと
case class Phantom[X, A](a: A)
case class PhantomInt[A](a: Int)
型変数Xがデータ構築子で使われていないことに注目します。
この使われていない型パラメータXを用いて、実際の値の型Aは同じでも、(X, A)の組合せでコンパイルチェックし
誤った値を渡してしまった場合等にコンパイルエラーにする手法です。
具体例を見てみましょう。
具体例
例として粗利益を計算する実装を考えてみます。
data Money a = Phantom a Int deriving Show
data Cost
data Sale
data Arari
arari :: Money Sale -> Money Cost -> Money Arari
arari (Money x) (Money y) = Money $ x - y
getSale :: Money Sale
getSale = Money 1000
getCost :: Money Cost
getCost = Money 500
main = do
putStrLn $ show $ arari getSale getCost -- OK
putStrLn $ show $ arari getCost getCost -- Compile Error
-- Couldn't match type ‘Cost’ with ‘Sale’
-- Expected type: Money Sale
-- Actual type: Money Cost
実際値としては同じIntの値でも
売上 - 原価は正しくコンパイルでき、原価 - 原価というような誤った計算をコンパイルエラーで落とせます。
※金額はIntではなく固定小数点を用いましょう。
他にも
case class Phantom[X, A](a: A)
trait Author {}
trait Post {}
def findAuthor(id: Phantom[Author, Long]): Author = /* ... */
def findPost(id: Phantom[Post, Long]): Post = /* ... */
findAuthor(Phantom[Author, Long](2)) // OK
findAuthor(Phantom[Post, Long](2))
/*
error: type mismatch;
found : Phantom[Post,Long]
required: Phantom[Author,Long]
findAuthor(Phantom[Post, Long](2))*/
こんな風にすれば、同じLongの値でも意味の異なるIDの渡し間違い等は全てコンパイルエラーで防いでくれます。
巨人の肩(型)に乗ろう
テストは「不具合があること」を立証できますが、「不具合がないこと」は立証できません。
型システムを活用するとこういった「ある種の不具合が無いこと」という悪魔の証明が可能になります。