幻影类型背后的动机



Don Stewart的Haskell在Large的演讲中提到了Phantom Types

data Ratio n = Ratio Double
1.234 :: Ratio D3
data Ask ccy = Ask Double
Ask 1.5123 :: Ask GBP

我读了他关于它们的要点,但我不明白。此外,我还阅读了有关该主题的Haskell维基。然而,我仍然没有抓住他们的观点。

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

回答"使用幻像类型的动机是什么"。有两点:

  • 使无效状态不可表示,这在 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)

在距离的情况下,我们可以计算出加法,例如,如果涉及不同的单位,则转换为公里。但这不适用于比率随时间变化不恒定的货币等。


可以

改用 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回答Expr 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

提到的可能就是这种情况。

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

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 IntBooleanNot必须始终返回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
因此,当您想要专门化数据构造函数的返回类型时,幻像构造函数很有用

,而幻像类型是其构造函数都是幻像构造函数的数据类型。


请注意,LeftRight 等数据构造函数也是幻像构造函数:

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,我们不显示十进制数字,而是使用千个逗号并将"10m"等输入解析为"10,000,000"。

在底层表示中,两者都只是Double

最新更新