是的!至少如果您允许自己使用 GHC 提供的一些语言扩展。您基本上有四种选择,一种不好,一种更好,一种不如其他两种明显,一种是 Right Way™。
1。坏的
你可以写
{-# 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 会因为你使用“错误功能”而对你大喊大叫。
2。更好
解决方案涉及开启 GADT。广义代数数据类型允许你做你想做的事。你的声明看起来像
{-# LANGUAGE GADTs #-}
data Point a where
Point :: Num a => a -> a -> a -> Point a
在使用 GADT 时,您可以通过声明其类型签名来声明构造函数,几乎就像在创建类型类时一样。
对 GADT 构造函数的约束有一个好处是它们可以传递到所创建的值——在这种情况下,这意味着你和编译器都知道唯一现有的Point as 有成员是Num as。因此,您可以将addPoint 函数编写为
addPoints :: Point a -> Point a -> Point a
addPoints (Point x1 y1 z1) {- ... -}
没有恼人的额外限制。
旁注:为 GADT 派生类
使用 GADT(或任何非 Haskell-98 类型)派生类需要额外的语言扩展,并且不像使用普通 ADT 那样顺利。原理是
{-# LANGUAGE StandaloneDeriving #-}
deriving instance Show (Point a)
这只会盲目地为 Show 类生成代码,您需要确保代码类型检查。
3。无名氏
正如 shachaf 在本文的 cmets 中指出的那样,您可以通过在 GHC 中启用 ExistentialQuantification 来获取 GADT 行为的相关部分,同时保留传统的 data 语法。这使得data 声明变得如此简单
{-# LANGUAGE ExistentialQuantification #-}
data Point a = Num a => Point a a a
4。正确的
但是,以上解决方案都不是社区的共识。如果您询问知识渊博的人(感谢 #haskell 频道中的 edwardk 和 startling 分享他们的知识),他们会告诉您根本不要限制您的类型。他们会告诉你,你应该将你的类型定义为
data Point a = Point a a a
然后约束在Points 上运行的任何函数,例如将两个点相加的函数:
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 as,这是不可能的,因为您无法为这种受限制的类型定义 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 约束,后一个示例将无法工作,原因有两个:
- 您将无法为您的 Point 定义 Functor 实例,并且
- 您根本无法在您的点中存储
Maybe a 坐标。
这只是一个有用的示例,如果您在该点上应用Num a 约束,您将放弃的许多示例。
另一方面,通过限制类型,您获得什么?我能想到三个原因:
“我不想意外地创建一个Point String 并尝试将它作为一个数字来操作。”你将无法做到。无论如何,类型系统都会阻止你。
“但这是出于文档目的!我想表明 Point 是数值的集合。” ...除非它不是,例如Point [-3, 3] [5] [2, 6],它表示轴上的替代坐标,这可能有效也可能不有效。
“我不想继续为我的所有函数添加Num 约束!”很公平。在这种情况下,您可以从ghci 复制和粘贴它们。在我看来,一点点键盘工作就值得所有好处。