幻影类型背后的动机?

时间:2015-01-31 02:30:31

标签: haskell phantom-types

Don Stewart的Haskell in the Large演讲中提到了幻影类型

data Ratio n = Ratio Double
1.234 :: Ratio D3

data Ask ccy = Ask Double
Ask 1.5123 :: Ask GBP

我读了他们关于他们的要点,但我不理解他们。另外,我阅读了有关该主题的Haskell Wiki。但我仍然错过了他们的观点。

使用幻像类型的动机是什么?

3 个答案:

答案 0 :(得分:65)

回答“使用幻影类型的动机是什么”。有两点:

  • 使无效状态无法代表,Aadit's answer
  • 对此进行了详细解释
  • 携带类型级别的部分信息

例如,您可以通过长度单位标记距离:

{-# 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

你可以避免Mars Climate Orbiter disaster

>>> 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)

在远距离情况下,我们可以计算出加法,例如,如果涉及不同的单位,则转换为公里。但这对于货币并不适用,其比率在一段时间内不会保持不变等。


而且可以使用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

这种方法特别简化了Aadit's answerExpr a示例:

{-# 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编译器 Don提到的情况。

答案 1 :(得分:14)

使用幻像类型的动机是专门化数据构造函数的返回类型。例如,考虑:

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 IntNil :: List Char)。


Haskell中的正常代数数据类型允许您选择数据构造函数的参数类型。例如,我们选择Cons以上aList 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

如您所见,所有数据构造函数的返回类型都是一般化的。这是有问题的,因为我们知道NumberIncrement必须始终返回Expr IntBoolean,而Not必须始终返回Expr Bool。< / p>

数据构造函数的返回类型是错误的,因为它们太笼统了。例如,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

当您想要专门化数据构造函数的返回类型时,幻像构造函数很有用,幻像类型是构造函数都是幻像构造函数的数据类型。


请注意,Rightdata Either a b = Left a | Right b Left :: a -> Either a b Right :: b -> Either a b 等数据构造函数也是幻像构造函数:

{{1}}

原因是虽然这些数据构造函数的返回类型确实取决于它们的参数,但它们仍然是一般化的,因为它们只部分依赖于它们的参数。

知道数据构造函数是否是幻像构造函数的简单方法:

  

数据构造函数的返回类型中出现的所有类型变量是否也出现在数据构造函数的参数中?如果是的话,它不是幻像构造函数。

希望有所帮助。

答案 2 :(得分:1)

具体来说,对于Ratio D3,我们使用类似的丰富类型来驱动类型导向的代码,例如如果在Ratio D3类型的某处有一个字段,则其编辑器将被分派到仅接受数字条目并显示3位精度的文本字段。这是相反的,例如,newtype Amount = Amount Double我们不显示十进制数字,但使用千位逗号和解析输入,例如&#39; 10m&#39;作为&#39; 10,000,000&#39;。

在基础表示中,两者仍然只是Double s。