web-dev-qa-db-ja.com

ファントムタイプの背後にある動機?

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 を読みました。しかし、私はまだ彼らの要点を逃しています。

ファントムタイプを使用する動機は何ですか?

45
Kevin Meredith

「ファントム型を使用する動機は何ですか」に答えるため。 2つのポイントがあります。

  • Aaditの答え でよく説明されている無効な状態を表現できないようにする
  • 型レベルの情報のいくつかを運ぶ

たとえば、長さの単位で距離をタグ付けできます。

{-# 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 aAaditの回答の例

{-# LANGUAGE GADTs #-}

data Expr a where
  Number     :: Int -> Expr Int
  Boolean    :: Bool -> Expr Bool
  Increment  :: Expr Int -> Expr Int
  Not        :: Expr Bool -> Expr Bool

後者のバリエーションには自明でない言語拡張(GADTsDataKindsKindSignatures)が必要であり、コンパイラでサポートされていない可能性があることを指摘する価値があります。 Muコンパイラの場合はそうかもしれません。

67
phadej

ファントム型を使用する背後にある動機は、データコンストラクターの戻り型を特殊化することです。たとえば、次のことを考慮してください。

data List a = Nil | Cons a (List a)

NilConsの両方の戻り値の型は、デフォルトで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

データコンストラクターの戻り値の型は一般的すぎるため、間違っています。たとえば、NumberExpr 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

したがって、ファントムコンストラクターは、データコンストラクターの戻り値の型を特殊化する場合に役立ちます。ファントム型は、コンストラクターがすべてファントムコンストラクターであるデータ型です。


LeftRightなどのデータコンストラクターもファントムコンストラクターであることに注意してください。

data Either a b = Left a | Right b

Left  :: a -> Either a b
Right :: b -> Either a b

その理由は、これらのデータコンストラクターの戻り値の型は引数に依存していますが、部分的にしか引数に依存していないため、一般化されているためです。

データコンストラクターがファントムコンストラクターかどうかを確認する簡単な方法:

データコンストラクターの戻りの型に現れるすべての型変数は、データコンストラクターの引数にも現れますか?もしそうなら、それはファントムコンストラクタではありません。

お役に立てば幸いです。

15
Aadit M Shah

具体的には、Ratio D3の場合、そのようなリッチタイプを使用して型指定コードを駆動します。タイプRatio D3のどこかにフィールドがある場合、そのエディターは、数値入力のみを受け入れ、3桁の精度を示すテキストフィールドにディスパッチされます。これは、たとえばnewtype Amount = Amount Doubleとは対照的です。ここでは、10進数は表示されませんが、1000のコンマを使用し、「10m」などの入力を「10,000,000」として解析します。

基本となる表現では、どちらもまだDoublesです。

1
Cactus