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
值得指出的是,后一种变体需要非平凡的语言扩展(GADTs
、DataKinds
、KindSignatures
),而编译器可能不支持这些扩展。Mu编译器Don
使用幻像类型背后的动机是专门化数据构造函数的返回类型。例如,考虑:
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
,我们不显示十进制数字,而是使用千个逗号并将"10m"等输入解析为"10,000,000"。
在底层表示中,两者都只是Double
。