幻影types背后的动机?
Don Stewart的Haskell在大型演讲中提到幻影types :
data Ratio n = Ratio Double 1.234 :: Ratio D3 data Ask ccy = Ask Double Ask 1.5123 :: Ask GBP
我读了他的关于他们的子弹点,但我不明白他们。 另外,我读了关于这个主题的Haskell Wiki 。 但是我仍然错过了他们的观点。
什么是使用幻影types的动机?
回答“使用幻像types的动机是什么”。 有两点:
- 使无效状态具有代表性,这在Aadit的回答中得到了很好的解释
- 提供types级别的一些信息
例如,您可以使用长度单位标记距离:
{-# 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
来closures一组单位:
{-# 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
这个方法特别简化了Expr a
从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提到的可能就是这种情况。
使用幻像types的动机是专门化返回types的数据构造函数。 例如,考虑:
data List a = Nil | Cons a (List a)
Nil
和Cons
的返回types默认为List a
(对于所有types为a
列表)。
Nil :: List a Cons :: a -> List a -> List a |____| | -- return type is generalized
还要注意, Nil
是一个幻影构造函数(即它的返回types不依赖于它的参数,在这种情况下是真实的,但是却是相同的)。
因为Nil
是一个幻影构造函数,所以我们可以将Nil
专门化为我们想要的任何types(例如Nil :: List Int
或者Nil :: List Char
)。
Haskell中的正规代数数据types允许您select数据构造函数的参数types。 例如,我们为上面的Cons
( a
和List a
)select了参数的types。
但是,它不允许您select数据构造函数的返回types。 返回types总是泛化的。 这在大多数情况下都可以。 但是,也有例外。 例如:
data Expr a = Number Int | Boolean Bool | Increment (Expr Int) | Not (Expr Bool)
数据构造函数的types是:
Number :: Int -> Expr a Boolean :: Bool -> Expr a Increment :: Expr Int -> Expr a Not :: Expr Bool -> Expr a
正如你所看到的,所有数据构造函数的返回types是一般化的。 这是有问题的,因为我们知道Number
和Increment
必须总是返回一个Expr Int
和Boolean
而Not
必须总是返回一个Expr Bool
。
数据构造函数的返回types是错误的,因为它们太笼统。 例如, Number
不可能返回一个Expr a
但它却可以。 这允许你写错误的expression式,types检查器不会捕获。 例如:
Increment (Boolean False) -- you shouldn't be able to increment a boolean Not (Number 0) -- you shouldn't be able to negate a number
问题是我们不能指定数据构造函数的返回types。
请注意, Expr
所有数据构造函数都是幻影构造函数(即它们的返回types不依赖于它们的参数)。 构造函数都是幻影构造函数的数据types称为幻影types。
请记住像Nil
这样的幻像构造函数的返回types可以专门用于我们想要的任何types。 因此,我们可以为Expr
创build一个聪明的构造函数,如下所示:
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
所以当幻想构造函数需要专门化数据构造函数的返回types时,幻像构造函数是有用的,而幻像types是构造函数都是幻影构造函数的数据types。
请注意像Left
和Right
这样的数据构造函数也是幻象构造函数:
data Either ab = Left a | Right b Left :: a -> Either ab Right :: b -> Either ab
原因在于虽然这些数据构造函数的返回types依赖于它们的论点,但它们仍然是泛化的,因为它们只是部分地依赖于它们的论点。
知道数据构造函数是否是幻影构造函数的简单方法:
所有出现在数据构造函数的返回types中的typesvariables是否也出现在数据构造函数的参数中? 如果是的话,这不是一个幻影的构造函数。
希望有所帮助。
对于Ratio D3
,我们使用类似这样的丰富types来驱动types定向代码,例如,如果在typesRatio D3
某处有一个字段,则其编辑器将被分派到只接受数字条目的文本字段,并显示3位数字的精度。 这与之相反,例如,新typesnewtype Amount = Amount Double
,其中我们不显示十进制数字,但使用千位逗号和parsinginput如“10m”为“10,000,000”。
在底层表示中,两者仍然只是Double
s。