我正在尝试创建一个数据类型,Point
它的构造函数需要三个数字。最初,我写了
data Point = Point Double Double Double
但是当某些代码预期时我遇到了一些问题Int
。
所以我把它改成了
data Point a = Point a a a
但现在我想强制这a
是一个实例(?)Num
- 我只想在构造函数中接受数字。
这可能吗?如果不是,公认的做法是什么?我用了多少次错误的词来描述某事?
是的!至少如果您允许自己使用 GHC 提供的一些语言扩展。您基本上有四种选择,一种不好,一种更好,一种不如其他两种明显,一种是 Right Way™。
你可以写
{-# LANGUAGE DatatypeContexts #-}
data Num a => Point a = Point a a a
这将使构造函数Point
只能用Num a
值调用。但是,它并不将值的内容限制Point
为Num a
值。这意味着,如果您想进一步增加两点,您仍然需要这样做
addPoints :: Num a => Point a -> Point a -> Point a
addPoints (Point x1 y1 z1) {- ... -}
你看到额外的Num a
声明了吗?这不应该是必要的,因为我们知道 a无论如何Point
都只能包含Num a
,但这就是DatatypeContexts
工作方式!无论如何,您必须对每个需要它的功能施加约束。
这就是为什么如果您启用DatatypeContexts
,GHC 会因为您使用“错误功能”而对您大喊大叫。
该解决方案涉及打开 GADT。广义代数数据类型允许你做你想做的事。你的声明看起来像
{-# LANGUAGE GADTs #-}
data Point a where
Point :: Num a => a -> a -> a -> Point a
使用 GADT 时,您通过声明其类型签名来声明构造函数,几乎就像在创建类型类时一样。
对 GADT 构造函数的约束的好处是它们可以传递给创建的值——在这种情况下,这意味着您和编译器都知道唯一现有Point a
的 s 具有 s 的成员Num a
。因此,您可以将addPoint
函数编写为
addPoints :: Point a -> Point a -> Point a
addPoints (Point x1 y1 z1) {- ... -}
没有恼人的额外约束。
使用 GADT(或任何非 Haskell-98 类型)派生类需要额外的语言扩展,并且不像使用普通 ADT 那样顺利。原理是
{-# LANGUAGE StandaloneDeriving #-}
deriving instance Show (Point a)
这只会盲目地为Show
类生成代码,并且由您来确保代码类型检查。
正如shachaf在这篇文章的评论中指出的那样,您可以通过在 GHC 中data
启用来获得 GADT 行为的相关部分,同时保留传统语法。ExistentialQuantification
这使得data
声明很简单
{-# LANGUAGE ExistentialQuantification #-}
data Point a = Num a => Point a a a
但是,以上解决方案都不是社区的共识。如果你问知识渊博的人(感谢edwardk和惊人的 #haskell 频道分享他们的知识),他们会告诉你根本不要限制你的类型。他们会告诉你,你应该将你的类型定义为
data Point a = Point a a a
然后约束在Point
s 上运行的任何函数,例如将两个点相加的函数:
addPoints :: Num a => Point a -> Point a -> Point a
addPoints (Point x1 y1 z1) {- ... -}
不限制您的类型的原因是,这样做时,您会严重限制以后使用类型的选择,这可能是您意想不到的。例如,为您的点创建一个 Functor 实例可能很有用,如下所示:
instance Functor Point where
fmap f (Point x y z) = Point (f x) (f y) (f z)
然后你可以通过简单地评估来做类似 aPoint Double
与 a的近似值Point Int
round <$> Point 3.5 9.7 1.3
这将产生
Point 4 10 1
Point a
如果您仅将 to 约束为s,则这是不可能Num a
的,因为您无法为这种受约束的类型定义 Functor 实例。您必须创建自己的pointFmap
函数,这将违背 Haskell 所代表的所有可重用性和模块化。
也许更有说服力,如果您向用户询问坐标但用户只输入其中两个,您可以将其建模为
Point (Just 4) (Just 7) Nothing
并通过映射轻松将其转换为 3D 空间中 XY 平面上的一个点
fromMaybe 0 <$> Point (Just 4) (Just 7) Nothing
这将返回
Point 4 7 0
Num a
请注意,如果您对自己的观点有限制,后一个示例将无法正常工作,原因有二:
Maybe a
在您的点中存储坐标。如果您在该点上应用约束,这只是您将放弃的许多示例中的一个有用的示例。Num a
另一方面,通过限制类型可以得到什么?我能想到三个原因:
“我不想意外地创建一个Point String
并试图将它作为一个数字来操纵。” 你将无法做到。无论如何,类型系统都会阻止你。
“但它是出于文档目的!我想表明 Point 是数值的集合。” ...除非它不是,例如Point [-3, 3] [5] [2, 6]
它表示轴上的替代坐标,这可能或可能不是全部有效。
“我不想继续Num
为我的所有功能添加约束!” 很公平。ghci
在这种情况下,您可以复制和粘贴它们。在我看来,一点点键盘工作值得所有好处。
您可以使用 GADT 来指定约束:
{-# Language GADTs #-}
data Point a where
Point :: (Num a) => a -> a -> a -> Point a
您可以使用Num
typeclassNum
对您的数据类型强制执行约束。通常的语法类似于:
data MyTypeClass a => MyDataType a = MyDataTypeConstructor1 a|MyDataTypeConstructor2 a a|{- and so on... -}
在你的情况下,你可以做
data Num a => Point a = Point a a a
阅读有关数据类型规范和LYAH的更多信息。Real World Haskell也提到了这一点。
编辑
正如shachaf所提到的,这不是很有效的 haskell2010,尽管规范提到了它。我还应该注意,这种形式很少使用,并且是通过函数而不是通过类型类/数据类型强制执行这些约束的首选方式,因为它们引入了对类型的额外依赖。