【问题标题】:How do I define a data type that only accepts numbers?如何定义只接受数字的数据类型?
【发布时间】:2013-11-08 02:54:00
【问题描述】:

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

data Point = Point Double Double Double

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

所以我改成

data Point a = Point a a a

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

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

【问题讨论】:

  • 正确的答案是将约束添加到使用该类型的事物中,例如addPoints :: Num a => Point a -> Point a -> Point a。不要将它作为数据类型声明本身的一部分来强制执行(有两种方法可以做到这一点;在这里两者都同样错误,尽管一种更平等)。
  • @shachaf 你能谈谈这背后的原因吗?
  • @sdasdadas 我现在已经在我的回答中详细说明了这一点。请参阅已接受答案中的第 4 点。

标签: haskell types typeclass


【解决方案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 specificationsLYAH 的更多信息。 Real World Haskell 也提到了这一点。

编辑

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

【讨论】:

  • 这是无效的 Haskell 2010。它是有效的 Haskell 98,但它在 Haskell 2010 中被删除,因为它没有做任何有用的事情。
  • @shachaf,我不知道 - 我的意思是,Haskell2010 仍然显示该信息(来自我的链接)。我会更新我的答案,谢谢。
【解决方案2】:

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

{-# Language GADTs #-}

data Point a where
  Point :: (Num a) => a -> a ->  a -> Point a 

【讨论】:

    【解决方案3】:

    是的!至少如果您允许自己使用 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 频道中的 edwardkstartling 分享他们的知识),他们会告诉您根本不要限制您的类型。他们会告诉你,你应该将你的类型定义为

    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 DoublePoint 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 约束,后一个示例将无法工作,原因有两个:

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

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

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

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

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

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

    【讨论】:

    • 没有必要为此引入 GADT 语法——你也可以写data Point a = Num a =&gt; Point a a a,只在 GHC 中使用 ExistentialQuantification。另一方面,对于这种情况,这仍然不是一个好的解决方案。当你使用 Point 值时,你应该只写约束。
    • @shachaf 毫不奇怪,GHC 的功能比我知道的要多!我也经常听到你的论点。有没有什么地方可以读到约束每个函数而不是构造函数的好处/缺点?创建data Pt = Pt Double Double Double 是否同样糟糕?如果这还不错,我很难理解为什么让类型更通用一点会很糟糕。 (但如果我能说服自己你所说的是正确的,我很乐意将它作为另一个替代词添加到我的答案中。)
    • 你不需要DatatypeContextsExistentialQuantification;真的,除了普通的旧数据类型上下文之外,你不需要它(感谢上帝)。
    • @AntalS-Z 谢谢,我写的那部分答案有点过早。 (在我玩弄ExistentialQuantification 以弄清楚它在这种情况下是如何工作的之前。)
    • 很好的答案,特别是如果应用于其他数据类型。对于点——好吧,如果你在坐标轴类型上参数化它们,那么我同意不应该限制类型。但是,我倾向于说首先参数化Point 可能并不好,因为这确实使它成为坐标类型的字面三重笛卡尔积。在处理仿射/向量空间时,这不一定是您要强调的数学观点。这些应该是基不变的,这只有在坐标类型接近ℝ(或ℂ,...)时才有可能。
    猜你喜欢
    • 1970-01-01
    • 2015-03-28
    • 1970-01-01
    • 2020-06-30
    • 2022-10-04
    • 2015-03-09
    • 2018-05-11
    • 1970-01-01
    • 2011-04-17
    相关资源
    最近更新 更多