Don Stewartの Haskell in the Large のプレゼンテーションについてPhantom Types:
data Ratio n = Ratio Double
1.234 :: Ratio D3
data Ask ccy = Ask Double
Ask 1.5123 :: Ask GBP
それらについて彼の箇条書きを読みましたが、理解できませんでした。また、このトピックについて Haskell Wiki を読みました。しかし、私はまだ彼らの要点を逃しています。
ファントムタイプを使用する動機は何ですか?
「ファントム型を使用する動機は何ですか」に答えるため。 2つのポイントがあります。
たとえば、長さの単位で距離をタグ付けできます。
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
newtype Distance a = Distance Double
deriving (Num, Show)
data Kilometer
data Mile
marathonDistance :: Distance Kilometer
marathonDistance = Distance 42.195
distanceKmToMiles :: Distance Kilometer -> Distance Mile
distanceKmToMiles (Distance km) = Distance (0.621371 * km)
marathonDistanceInMiles :: Distance Mile
marathonDistanceInMiles = distanceKmToMiles marathonDistance
そして、あなたは 火星気候オービター災害 を避けることができます:
>>> marathonDistanceInMiles
Distance 26.218749345
>>> marathonDistanceInMiles + marathonDistance
<interactive>:10:27:
Couldn't match type ‘Kilometer’ with ‘Mile’
Expected type: Distance Mile
Actual type: Distance Kilometer
In the second argument of ‘(+)’, namely ‘marathonDistance’
In the expression: marathonDistanceInMiles + marathonDistance
この「パターン」には若干のバリエーションがあります。 DataKinds
を使用すると、閉じた単位のセットを使用できます。
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE DataKinds #-}
data LengthUnit = Kilometer | Mile
newtype Distance (a :: LengthUnit) = Distance Double
deriving (Num, Show)
marathonDistance :: Distance 'Kilometer
marathonDistance = Distance 42.195
distanceKmToMiles :: Distance 'Kilometer -> Distance 'Mile
distanceKmToMiles (Distance km) = Distance (0.621371 * km)
marathonDistanceInMiles :: Distance 'Mile
marathonDistanceInMiles = distanceKmToMiles marathonDistance
そしてそれは同様に機能します:
>>> marathonDistanceInMiles
Distance 26.218749345
>>> marathonDistance + marathonDistance
Distance 84.39
>>> marathonDistanceInMiles + marathonDistance
<interactive>:28:27:
Couldn't match type ‘'Kilometer’ with ‘'Mile’
Expected type: Distance 'Mile
Actual type: Distance 'Kilometer
In the second argument of ‘(+)’, namely ‘marathonDistance’
In the expression: marathonDistanceInMiles + marathonDistance
ただし、Distance
はキロメートルまたはマイル単位でしか設定できないため、後で単位を追加することはできません。これは、一部のユースケースで役立つ場合があります。
次のこともできます:
data Distance = Distance { distanceUnit :: LengthUnit, distanceValue :: Double }
deriving (Show)
距離の場合、加算を計算できます。たとえば、異なる単位が含まれる場合はキロメートルに変換できます。しかし、これはcurrenciesの場合、うまく機能しません。
そして、代わりにGADTを使用することが可能です。これは、状況によってはより簡単な方法になる場合があります。
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE StandaloneDeriving #-}
data Kilometer
data Mile
data Distance a where
KilometerDistance :: Double -> Distance Kilometer
MileDistance :: Double -> Distance Mile
deriving instance Show (Distance a)
marathonDistance :: Distance Kilometer
marathonDistance = KilometerDistance 42.195
distanceKmToMiles :: Distance Kilometer -> Distance Mile
distanceKmToMiles (KilometerDistance km) = MileDistance (0.621371 * km)
marathonDistanceInMiles :: Distance Mile
marathonDistanceInMiles = distanceKmToMiles marathonDistance
これで、値レベルでも単位がわかります。
>>> marathonDistanceInMiles
MileDistance 26.218749345
このアプローチは特にExpr a
Aaditの回答の例 :
{-# LANGUAGE GADTs #-}
data Expr a where
Number :: Int -> Expr Int
Boolean :: Bool -> Expr Bool
Increment :: Expr Int -> Expr Int
Not :: Expr Bool -> Expr Bool
後者のバリエーションには自明でない言語拡張(GADTs
、DataKinds
、KindSignatures
)が必要であり、コンパイラでサポートされていない可能性があることを指摘する価値があります。 Muコンパイラの場合はそうかもしれません。
ファントム型を使用する背後にある動機は、データコンストラクターの戻り型を特殊化することです。たとえば、次のことを考慮してください。
data List a = Nil | Cons a (List a)
Nil
とCons
の両方の戻り値の型は、デフォルトでList a
です(これは、型a
のすべてのリストに対して一般化されています)。
Nil :: List a
Cons :: a -> List a -> List a
|____|
|
-- return type is generalized
また、Nil
はファントムコンストラクターであることに注意してください(つまり、戻り値の型は引数に依存していません。この場合は空想ですが、それでも同じです)。
Nil
はファントムコンストラクターであるため、Nil
を任意の型(Nil :: List Int
またはNil :: List Char
など)に特化できます。
Haskellの通常の代数データ型を使用すると、データコンストラクターの引数の型を選択できます。たとえば、上のCons
の引数のタイプを選択しました(a
およびList a
)。
ただし、データコンストラクターの戻り値の型を選択することはできません。戻り値の型は常に一般化されています。ほとんどの場合これで問題ありません。ただし、例外があります。例えば:
data Expr a = Number Int
| Boolean Bool
| Increment (Expr Int)
| Not (Expr Bool)
データコンストラクターのタイプは次のとおりです。
Number :: Int -> Expr a
Boolean :: Bool -> Expr a
Increment :: Expr Int -> Expr a
Not :: Expr Bool -> Expr a
ご覧のとおり、すべてのデータコンストラクターの戻り値の型は一般化されています。 Number
およびIncrement
は常にExpr Int
を返す必要があり、Boolean
およびNot
は常にExpr Bool
。
データコンストラクターの戻り値の型は一般的すぎるため、間違っています。たとえば、Number
はExpr a
を返すことはできませんが、返します。これにより、型チェッカーがキャッチしない間違った式を書くことができます。例えば:
Increment (Boolean False) -- you shouldn't be able to increment a boolean
Not (Number 0) -- you shouldn't be able to negate a number
問題は、データコンストラクターの戻り値の型を指定できないことです。
Expr
のすべてのデータコンストラクターはファントムコンストラクターです(つまり、戻り値の型は引数に依存しません)。コンストラクターがすべてファントムコンストラクターであるデータ型は、ファントムタイプと呼ばれます。
Nil
のようなファントムコンストラクターの戻り値の型は、任意の型に特化できることに注意してください。したがって、次のようにExpr
のスマートコンストラクターを作成できます。
number :: Int -> Expr Int
boolean :: Bool -> Expr Bool
increment :: Expr Int -> Expr Int
not :: Expr Bool -> Expr Bool
number = Number
boolean = Boolean
increment = Increment
not = Not
これで、通常のコンストラクターの代わりにスマートコンストラクターを使用でき、問題が解決します。
increment (boolean False) -- error
not (number 0) -- error
したがって、ファントムコンストラクターは、データコンストラクターの戻り値の型を特殊化する場合に役立ちます。ファントム型は、コンストラクターがすべてファントムコンストラクターであるデータ型です。
Left
やRight
などのデータコンストラクターもファントムコンストラクターであることに注意してください。
data Either a b = Left a | Right b
Left :: a -> Either a b
Right :: b -> Either a b
その理由は、これらのデータコンストラクターの戻り値の型は引数に依存していますが、部分的にしか引数に依存していないため、一般化されているためです。
データコンストラクターがファントムコンストラクターかどうかを確認する簡単な方法:
データコンストラクターの戻りの型に現れるすべての型変数は、データコンストラクターの引数にも現れますか?もしそうなら、それはファントムコンストラクタではありません。
お役に立てば幸いです。
具体的には、Ratio D3
の場合、そのようなリッチタイプを使用して型指定コードを駆動します。タイプRatio D3
のどこかにフィールドがある場合、そのエディターは、数値入力のみを受け入れ、3桁の精度を示すテキストフィールドにディスパッチされます。これは、たとえばnewtype Amount = Amount Double
とは対照的です。ここでは、10進数は表示されませんが、1000のコンマを使用し、「10m」などの入力を「10,000,000」として解析します。
基本となる表現では、どちらもまだDouble
sです。