幻影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 

值得指出的是,后者的变体需要非重要的语言扩展( GADTsDataKindsKindSignatures ),这在编译器中可能不被支持。 Mu编译器 Don提到的可能就是这种情况。

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

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

NilCons的返回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。 例如,我们为上面的ConsaList 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是一般化的。 这是有问题的,因为我们知道NumberIncrement必须总是返回一个Expr IntBooleanNot必须总是返回一个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。


请注意像LeftRight这样的数据构造函数也是幻象构造函数:

 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。