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

sda*_*das 14 haskell types typeclass

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

data Point = Point Double Double Double
Run Code Online (Sandbox Code Playgroud)

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

所以我改成了

data Point a = Point a a a
Run Code Online (Sandbox Code Playgroud)

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

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

kqr*_*kqr 26

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

1.坏

你可以写

{-# LANGUAGE DatatypeContexts #-}
data Num a => Point a = Point a a a
Run Code Online (Sandbox Code Playgroud)

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

addPoints :: Num a => Point a -> Point a -> Point a
addPoints (Point x1 y1 z1) {- ... -}
Run Code Online (Sandbox Code Playgroud)

你看到额外的Num a声明吗?这不应该是必要的,因为我们知道无论如何Point只能包含Num a,但这就是DatatypeContexts工作方式!无论如何,你必须对每个需要它的函数施加约束.

这就是为什么,如果你启用DatatypeContexts,GHC会因为使用"错误"而尖叫一下你.

2.更好

解决方案涉及开启GADT.广义代数数据类型允许您执行您想要的操作.你的声明会是这样的

{-# LANGUAGE GADTs #-}
data Point a where
  Point :: Num a => a -> a -> a -> Point a
Run Code Online (Sandbox Code Playgroud)

使用GADT时,您可以通过声明类型签名来声明构造函数,就像创建类型类时一样.

对GADT构造函数的约束具有以下优点:它们可以延续到创建的值 - 在这种情况下,这意味着您编译器都知道唯一存在的Point as具有s的成员Num a.因此,您可以将您的addPoint功能编写为正确

addPoints :: Point a -> Point a -> Point a
addPoints (Point x1 y1 z1) {- ... -}
Run Code Online (Sandbox Code Playgroud)

没有刺激性的额外约束.

附注:为GADT派生类

使用GADT(或任何非Haskell-98类型)派生类需要额外的语言扩展,并且它不像普通ADT那样平稳.原则是

{-# LANGUAGE StandaloneDeriving #-}
deriving instance Show (Point a)
Run Code Online (Sandbox Code Playgroud)

这将只是盲目地为Show类生成代码,并且由您来确保代码类型检查.

3.朦胧

正如shachaf在本文的评论中指出的那样,您可以data通过ExistentialQuantification在GHC中启用来保留传统语法,从而获得GADT行为的相关部分.这使data声明变得简单

{-# LANGUAGE ExistentialQuantification #-}
data Point a = Num a => Point a a a
Run Code Online (Sandbox Code Playgroud)

4.正确

但是,上述解决方案都不是社区的共识.如果你问知识渊博的人(感谢edwardk并在#haskell频道中惊讶地分享他们的知识),他们会告诉你不要限制你的类型.他们会告诉您应该将类​​型定义为

data Point a = Point a a a
Run Code Online (Sandbox Code Playgroud)

然后约束在Points上运行的任何函数,例如将两个点加在一起的函数:

addPoints :: Num a => Point a -> Point a -> Point a
addPoints (Point x1 y1 z1) {- ... -}
Run Code Online (Sandbox Code Playgroud)

不限制类型的原因是,在这样做时,您会严格限制以后使用类型的选项,这可能是您可能没想到的.例如,为您的点创建Functor实例可能很有用,如下所示:

instance Functor Point where
  fmap f (Point x y z) = Point (f x) (f y) (f z)
Run Code Online (Sandbox Code Playgroud)

然后,你可以这样做近似Point Double具有Point Int通过简单评测

round <$> Point 3.5 9.7 1.3
Run Code Online (Sandbox Code Playgroud)

这将产生

Point 4 10 1
Run Code Online (Sandbox Code Playgroud)

如果仅限制Point aNum as,则无法进行此操作,因为您无法为此类约束类型定义Functor实例.您必须创建自己的pointFmap功能,这将违背Haskell所代表的所有可重用性和模块性.

也许更有说服力,如果你问用户坐标但用户只输入其中两个,你可以将其建模为

Point (Just 4) (Just 7) Nothing
Run Code Online (Sandbox Code Playgroud)

并通过映射将其轻松转换为3D空间中XY平面上的点

fromMaybe 0 <$> Point (Just 4) (Just 7) Nothing
Run Code Online (Sandbox Code Playgroud)

将返回

Point 4 7 0
Run Code Online (Sandbox Code Playgroud)

请注意,如果你Num a对你的观点有一个约束,那后面的例子将不会有两个原因:

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

而这仅仅是一个,如果你的应用,你会放弃很多有用的例子Num a在点约束.

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

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

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

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


Ank*_*kur 5

您可以使用GADT指定约束:

{-# Language GADTs #-}

data Point a where
  Point :: (Num a) => a -> a ->  a -> Point a 
Run Code Online (Sandbox Code Playgroud)