15

我正在尝试创建一个数据类型,Point它的构造函数需要三个数字。最初,我写了

data Point = Point Double Double Double

但是当某些代码预期时我遇到了一些问题Int

所以我把它改成了

data Point a = Point a a a

但现在我想强制这a是一个实例(?)Num- 我只想在构造函数中接受数字。

这可能吗?如果不是,公认的做法是什么?我用了多少次错误的词来描述某事?

4

3 回答 3

29

是的!至少如果您允许自己使用 GHC 提供的一些语言扩展。您基本上有四种选择,一种不好,一种更好,一种不如其他两种明显,一种是 Right Way™。

1. 坏的

你可以写

{-# LANGUAGE DatatypeContexts #-}
data Num a => Point a = Point a a a

这将使构造函数Point只能用Num a值调用。但是,它并不将值的内容限制PointNum a值。这意味着,如果您想进一步增加两点,您仍然需要这样做

addPoints :: Num a => Point a -> Point a -> Point a
addPoints (Point x1 y1 z1) {- ... -}

你看到额外的Num a声明了吗?这不应该是必要的,因为我们知道 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 a的 s 具有 s 的成员Num a。因此,您可以将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在这篇文章的评论中指出的那样,您可以通过在 GHC 中data启用来获得 GADT 行为的相关部分,同时保留传统语法。ExistentialQuantification这使得data声明很简单

{-# LANGUAGE ExistentialQuantification #-}
data Point a = Num a => Point a a a

4.正确的

但是,以上解决方案都不是社区的共识。如果你问知识渊博的人(感谢edwardk惊人的 #haskell 频道分享他们的知识),他们会告诉你根本不要限制你的类型。他们会告诉你,你应该将你的类型定义为

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)

然后你可以通过简单地评估来做类似 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请注意,如果您对自己的观点有限制,后一个示例将无法正常工作,原因有二:

  1. 您将无法为您的 Point 定义 Functor 实例,并且
  2. 您根本无法Maybe a在您的点中存储坐标。

如果您在该点上应用约束,这只是您将放弃的许多示例中的一个有用的示例。Num a

另一方面,通过限制类型可以得到什么?我能想到三个原因:

  1. “我不想意外地创建一个Point String并试图将它作为一个数字来操纵。” 你将无法做到。无论如何,类型系统都会阻止你。

  2. “但它是出于文档目的!我想表明 Point 是数值的集合。” ...除非它不是,例如Point [-3, 3] [5] [2, 6]它表示轴上的替代坐标,这可能或可能不是全部有效。

  3. “我不想继续Num为我的所有功能添加约束!” 很公平。ghci在这种情况下,您可以复制和粘贴它们。在我看来,一点点键盘工作值得所有好处。

于 2013-10-29T05:28:22.267 回答
5

您可以使用 GADT 来指定约束:

{-# Language GADTs #-}

data Point a where
  Point :: (Num a) => a -> a ->  a -> Point a 
于 2013-10-29T05:25:45.953 回答
1

您可以使用NumtypeclassNum对您的数据类型强制执行约束。通常的语法类似于:

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,尽管规范提到了它。我还应该注意,这种形式很少使用,并且是通过函数而不是通过类型类/数据类型强制执行这些约束的首选方式,因为它们引入了对类型的额外依赖。

于 2013-10-29T05:15:30.180 回答