我正在尝试创建一个数据类型Point
,它的构造函数需要三个数字。最初,我写过
data Point = Point Double Double Double
但是当某些代码需要Int
时,我遇到了一些问题。
所以我把它改成了
data Point a = Point a a a
但现在我想强制a
是Num
的实例(?) - 我只想接受构造函数中的数字。
这可能吗?如果没有,接受的做法是什么?我用了多少次错误的词来描述某些东西?
答案 0 :(得分:26)
是的!至少如果你允许自己一些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
声明?这不应该是必要的,因为我们知道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
成员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
上运行的任何函数,例如将两个点一起添加的函数:
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)
然后您可以通过简单评估
来执行类似Point Double
与Point Int
近似的操作
round <$> Point 3.5 9.7 1.3
将产生
Point 4 10 1
如果您仅将Point a
限制为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
复制并粘贴它们。在我看来,一点键盘工作是值得的所有好处。
答案 1 :(得分:5)
您可以使用GADT指定约束:
{-# Language GADTs #-}
data Point a where
Point :: (Num a) => a -> a -> a -> Point a
答案 2 :(得分:1)
您可以使用Num
类型类对您的数据类型强制执行Num
约束。通常的语法类似于:
data MyTypeClass a => MyDataType a = MyDataTypeConstructor1 a|MyDataTypeConstructor2 a a|{- and so on... -}
在您的情况下,您可以
data Num a => Point a = Point a a a
详细了解data type specifications和LYAH。 Real World Haskell也提到了这一点。
修改强>
正如shachaf所提到的,虽然规范提到它,但这并不是很有效的haskell2010。我还应该注意,这种形式很少被使用,并且是通过函数而不是通过类型类/数据类型来强制执行这些约束的首选方法,因为它们引入了对类型的额外依赖性。