如何定义仅接受数字的数据类型?

时间:2013-10-29 04:56:34

标签: haskell types typeclass

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

data Point = Point Double Double Double

但是当某些代码需要Int时,我遇到了一些问题。

所以我把它改成了

data Point a = Point a a a

但现在我想强制aNum的实例(?) - 我只想接受构造函数中的数字。

这可能吗?如果没有,接受的做法是什么?我用了多少次错误的词来描述某些东西?

3 个答案:

答案 0 :(得分:26)

是的!至少如果你允许自己一些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 a成员Num a。因此,您可以将addPoint函数编写为

addPoints :: Point a -> Point a -> Point a
addPoints (Point x1 y1 z1) {- ... -}
没有刺激性的额外约束。

附注:为GADTs派生类

使用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

然后约束在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 DoublePoint 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约束,那后面的例子就不会有两个原因:

  1. 您无法为Point定义Functor实例,
  2. 您根本无法将Maybe a坐标存储在您的观点中。
  3. 如果您对该点应用了Num a约束,那么这只是一个有用的例子。

    另一方面,你通过约束你的类型获得是什么?我可以想到三个原因:

    1. “我不想意外创建Point String,并尝试将其作为数字进行操作。”你将无法做到。无论如何,类型系统都会阻止你。

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

    3. “我不想继续为我的所有功能添加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 specificationsLYAHReal World Haskell也提到了这一点。

修改

正如shachaf所提到的,虽然规范提到它,但这并不是很有效的haskell2010。我还应该注意,这种形式很少被使用,并且是通过函数而不是通过类型类/数据类型来强制执行这些约束的首选方法,因为它们引入了对类型的额外依赖性。