我如何定义只接受数字的数据类型? [英] How do I define a data type that only accepts numbers?

查看:101
本文介绍了我如何定义只接受数字的数据类型?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

  data Point = Double Double Double 

但是当某些代码片段期望 Int s时遇到了一些问题。



所以我将它改为

 数据点a =点aaa 

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



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

是的!至少如果你允许你自己提供一些GHC提供的语言扩展。你基本上有四个选择,一个不好,一个更好,一个不像其他两个那么明显,一个是正确的方式(Right Way™)。

1。您可以编写

  { - #LANGUAGE DatatypeContexts# - } 
data Num a =>点a =点aaa

这将使得构造函数 Point 只能用 Num a 值来调用。但是,它不会将 Point 值的内容限制为 Num a 值。这意味着,如果你在路上想要增加两点,你仍然必须这样做:

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

你会看到额外的 Num a 声明?这应该是不必要的,因为我们知道 Point 只能包含 Num a ,但这就是 DatatypeContexts 工作!这就是为什么,如果你启用了 DatatypeContexts ,那么GHC会因为使用misfeature而尖叫一下。

2。更好



解决方案包括打开GADT。通用的代数数据类型允许你做你想做的事情。您的声明将如下所示:

  { - #LANGUAGE GADTs# - } 
data点a其中
Point :: Num a => a - > a - > a - >指向一个

使用GADT时,通过声明它们的类型签名来声明构造函数,就像创建类型类时。



对GADT构造函数的约束有一个好处,它可以继承创建的值 - 在这种情况下,这意味着您编译器知道唯一存在的 Point a s的成员是 Num a s。因此,您可以将 addPoint 函数编写为

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

刺激的额外约束。
$ b

附注:为GADT派生类



使用GADT(或任何非-Haskell-98型)需要额外的语言扩展,并且不像普通的ADT那样顺畅。原理是:
$ b $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ $ {$#$ $ $ $ $ {$#

这只会盲目地为 Show 类生成代码,这是由你来确保代码typechecks。



3。 The Obscure



As shachaf 在这篇文章的评论中指出,你可以获得GADT行为的相关部分,同时保留传统的<$ c通过在GHC中启用 ExistentialQuantification 来实现$ c> data 语法。这使得 data 声明就像

  { - #LANGUAGE ExistentialQuantification # - } 
data point a = Num a =>指出一个



4。正确



然而,上述解决方案都不是社群中的共识。如果你问知识渊博的人(感谢#haskell频道的 edwardk 惊人的分享他们的知识),他们会告诉你根本不约束你的类型即可。他们会告诉你,你应该将你的类型定义为

 数据点a =点aaa 

然后约束在 Point s上运行的任何函数,例如要添加的函数两点一起:

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

不要限制你的类型的原因是,当你这样做的时候,你认真地限制你稍后使用类型的选项,你可能并不期望。例如,为你的观点创建一个Functor实例可能是有用的,如下所示:

  instance Functor Point其中
fmap f(Point xyz)= Point(fx)(fy)(fz)

然后你可以做类似于通过简单评估
$ b $来近似一个 Point Double 与一个 Point Int b

  round <$> Point 3.5 9.7 1.3 

这将产生

 点4 10 1 

如果你仅将指向a 限制为>仅限数量 s,因为您无法为这种约束类型定义Functor实例。您必须创建您自己的 pointFmap 函数,这将违背Haskell所代表的所有可重用性和模块性。



也许更令人信服的是,如果你向用户询问坐标,但用户只输入其中的两个,你可以将它建模为 c> Point(Just 4)(Just 7)Nothing

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

  fromMaybe 0< $> Point(Just 4)(Just 7)Nothing 

返回

 点4 7 0 

注在这里,后面的例子不会有两个原因,如果你有一个 Num a 约束你的观点:


  1. 您无法为您的Point定义Functor实例,并且
  2. 您完全不能存储 Maybe

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



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


  1. 我不想意外地创建点字符串并尝试将它作为一个数字操作。你将无法做到。不管怎样,类型系统都会阻止你。


  2. 但是它只是为了文档的目的!我想表明一个Point是一个数值的集合。 ...除非它不是,例如点[-3,3] [5] [2,6] ,它表示轴上的替代坐标,可能并非全部有效。

  3. 我的功能!很公平。在这种情况下,您可以从 ghci 复制并粘贴它们。在我看来,一个小小的键盘工作是值得所有好处的。


I am attempting to create a data type, Point, that takes three numbers for its constructor. Initially, I had written

data Point = Point Double Double Double

but I ran into some issues when certain pieces of code expected Ints.

So I changed it to

data Point a = Point a a a

but now I would like to enforce that a is an instance (?) of Num - I only want to accept numbers in the constructor.

Is this possible? If not, what is the accepted practice? How many times did I use the wrong word to describe something?

解决方案

Yes! At least if you allow yourself some language extensions GHC provide. You basically have four options where one is bad, one is better, one is not as obvious as the other two and one is the Right Way™.

1. The Bad

You can write

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

This will make it so that the constructor Point can only be called with Num a values. However, it does not limit the contents of a Point value to Num a values. This means that if you further down the road want to add two points, you would still have to do

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

Do you see the extra Num a declaration? That shouldn't be necessary since we know a Point can only contain Num a anyway, but that's the way DatatypeContexts work! You have to put constraints on every function needing it anyway.

This is why, if you enable DatatypeContexts, GHC will scream at you a little for using a "misfeature."

2. The Better

The solution involves turning on GADTs. Generalised algebraic datatypes allow you to do what you want. Your declaration would then look like

{-# LANGUAGE GADTs #-}
data Point a where
  Point :: Num a => a -> a -> a -> Point a

When using GADTs, you declare constructors by stating their type signature instead, almost like when creating typeclasses.

Constraints on GADT constructors have the benefit that they carry over to the value that is created – in this case that means both you and the compiler knows that the only existing Point as have members who are Num as. You can therefore write your addPoint function as just

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

without the irritating extra constraint.

Side note: Deriving Classes for GADTs

Deriving classes with GADTs (or any non-Haskell-98 type) requires an extra language extension and it is not as smooth sailing as with normal ADTs. The principle is

{-# LANGUAGE StandaloneDeriving #-}
deriving instance Show (Point a)

This will just blindly generate code for the Show class, and it is up to you to make sure that code typechecks.

3. The Obscure

As shachaf points out in the comments to this post, you can get the relevant parts of GADT behaviour while retaining traditional data syntax by enabling ExistentialQuantification in GHC. This makes the data declaration as simple as

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

4. The Correct

However, none of the solutions above is what the consensus in the community is. If you ask knowledgeable people (thanks to edwardk and startling in the #haskell channel for sharing their knowledge), they will tell you not to constrain your types at all. They will tell you that you should define your type as

data Point a = Point a a a

and then constrain any functions operating on Points, like for example the one to add two points together:

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

The reason to not constrain your types is that when doing so, you seriously limit your options for using the types later, in ways you probably don't expect. For example, creating a Functor instance for your point might be useful, like so:

instance Functor Point where
  fmap f (Point x y z) = Point (f x) (f y) (f z)

and then you can do something like approximating a Point Double with a Point Int by simply evaluating

round <$> Point 3.5 9.7 1.3

which will produce

Point 4 10 1

This would not be possible if you constrained your Point a to Num as only, because you can't define a Functor instance for such a constrained type. You woud have to create your own pointFmap function, which would go against all reusability and modularity that Haskell stands for.

Perhaps even more convincing, if you ask the user for coordinates but the user only enters two of them, you can model that as a

Point (Just 4) (Just 7) Nothing

and easily convert it to a point on the XY plane in 3D space by mapping

fromMaybe 0 <$> Point (Just 4) (Just 7) Nothing

which will return

Point 4 7 0

Note here that this latter example wouldn't work for two reasons if you had a Num a constraint on your point:

  1. You would not be able to define a Functor instance for your Point, and
  2. You would not at all be able to store Maybe a coordinates in your point.

And this is just one useful example of the many you would forego if you applied the Num a constraint on the point.

On the flip side of this, what do you gain by constraining your types? I can think of three reasons:

  1. "I don't want to accidentally create a Point String and try to manipulate it as a number." You won't be able to. The type system will stop you anyway.

  2. "But it's for documentation purposes! I want to show that a Point is a collection of numeric values." ...except when it is not, such as Point [-3, 3] [5] [2, 6] which expresses alternative coordinates on the axes, which may or may not all be valid.

  3. "I don't want to keep adding Num constraints to all my functions!" Fair enough. You can copy and paste them from ghci in this case. A little keyboard work is worth all the benefits, in my opinion.

这篇关于我如何定义只接受数字的数据类型?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆