2017月07月07日

Phantom Typeの紹介

幽霊型とは

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の渡し間違い等は全てコンパイルエラーで防いでくれます。

巨人の肩(型)に乗ろう

テストは「不具合があること」を立証できますが、「不具合がないこと」は立証できません。
型システムを活用するとこういった「ある種の不具合が無いこと」という悪魔の証明が可能になります。

RELATED POSTS