Haskell类型同义词声明是否可以带有约束条件?

9
假设我想为所有由 Int 组成的列表创建类型同义词。
我可以这样做:
```haskell type MyList = [Int] ```
这将使 `MyList` 成为 `[Int]` 的同义词。
type NumberList = [Int]

但是,如果我想调用所有包含数字的列表并将它们命名为NumberList,我该如何设置约束条件并说所有[a]只要"Num a"就应该被称为同一个名称?

编辑::在看到答案后,我重新考虑了一下。这似乎违背了Haskell背后的基本思想之一,而且回报相对较小(只是一个正式问题)。我决定这样做:如果一个类型需要两个完全相同的实例,只有Int或Float不同,那么它们之间的差异太小,无法证明需要使用两者的解决方法,因此我必须限制使用其中之一。然而,如果有重要原因需要同时使用两者,则可以通过以下方式避免问题:

data Thing = Thing_A(String, String Float) | Thing_B(String,String,Int)

---并且保持Haskell的类型系统,同时将它们都作为数据类型Thing接受。我最初想做的是

data Thing = Thing(String, String, Float) | Thing(String, String, Int)

你希望 NumberList 类型的值能够包含任何数字列表,或者包含任何数字的列表,还是具有多态性并可用作任何数值类型的列表? - hzap
一个“某个Num a的a”类型会非常无用,因为Num类型类不允许您以任何方式取回数字。您想对这样的列表做什么? - hzap
我会查看这些答案...我想做什么?我想创建一个数据类型,其中构造函数中的一个值是[(Char, Int)],但在某些情况下,也可能需要使用[(Char, Float)],我希望它们都应该被归类为我的类型Profile(请记住,还有其他构造函数值,不仅仅是元组列表)。 - fast-reflexes
4个回答

10

这对应于存在量词。在伪Haskell中,

type NumberList = exists a . Num a => [a]

我说“伪”,因为GHC不允许即时引入存在量词——你需要为此创建一个单独的数据类型。

现在,大多数情况下,你会在箭头左侧使用NumberList的类型,在这里,“存在”有效地改变了它的含义为“对于所有”的意思。

也就是说,你不是写:

isIncreasing :: NumberList -> Bool

这与

isIncreasing :: (exists a . Num a => [a]) -> Bool

你可以编写

isIncreasing :: forall a . Num a => [a] -> Bool

或者简单地说

isIncreasing :: Num a => [a] -> Bool

当然,使用类型同义词似乎代码量更少,但它也有缺点。

顺便提一下,这些缺点在面向对象编程中很典型,因为它是基于存在论方法的。

例如,您想连接两个列表。通常你会写

(++) :: forall a . [a] -> [a] -> [a]

(这里的 forall 只是为了明确而不必要。) 由于 a 在整个签名中都是相同的,这确保你正在连接相同类型的列表。

我们如何连接两个数字列表?一个签名:

(++) :: NumberList -> NumberList -> NumberList

这样做不起作用,因为一个列表可能包含Ints而另一个列表可能包含Doubles。 而且生成的NumberList必须包含单个类型的值。

或者说,您想找到列表元素的总和。

通常您会写

sum :: Num a => [a] -> a

请注意,结果类型与列表元素的类型相同。遗憾的是,我们不能对NumberList做同样的操作!

sum :: NumberList -> ???

结果类型是什么?我们也可以在那里应用存在量词。

sum :: NumberList -> (exists a . Num a => a)

但是现在原始列表类型和求和类型之间的连接已经丢失了——至少对于Haskell的类型系统而言。如果你随后决定编写一个函数,比如:

multiplySum :: Integer -> [Integer] -> Integer
multiplySum x ys = x * sum ys

如果这样做,你会得到一个类型错误,因为sum ys可能是任何类型,不一定是整数类型。

如果将所有类型存在量化,并将其推向极端,它也可以工作-但那样你最终会得到另一种面向对象的语言,具有它们所有的问题。

(当然,存在量化也有一些很好的用例。)


感谢有效地演示了我应该重新考虑的原因,我已经这样做了...非常感谢! :) - fast-reflexes

7

使用数据和参数获取上下文,而不是存在性

我认为如果你想要

data Thing = Good [(Char,Int)] | Bad String | Indifferent Leg

但有时候也会

data Thing = Good [(Char,Float)] | Bad String | Indifferent Arm

您可以定义

data Thing num bodypart = Good [(Char,num)] | Bad String | Indifferent bodypart

或者,如果您想确保num始终为数字,可以执行以下操作:

data Num num => Thing num bodypart = Good [(Char,num)] | Bad String | Indifferent bodypart

最后,您可以通过定义自己的类来限制bodypart中允许的类型:

class Body a where
   -- some useful function(s) here

instance Body Leg where
   -- define useful function(s) on Legs
instance Body Arm
   -- define useful function(s) on Arms

data (Num num,Body bodypart) => Thing num bodypart = 
                                                             Good [(Char,num)] | Bad String | Indifferent bodypart

我建议你不要使用forall构造函数或GADTs来使用存在类型,因为将num参数添加到数据类型中在实践中更加有用,尽管需要打字较多。

类型同义词的约束条件?

请注意,当您使用像

data (Num num) => Thing num = T [(Char,num)]

实际上只是改变了构造函数T的类型

T :: (Num num) => [(Char,num)] -> Thing num

与其使用T :: [(Char,num)] -> Thing num,不如使用T :: (Num num) => [(Char,num)] -> Thing num。这意味着每次使用T时,需要一个上下文(Num num),但这正是您想要的——防止将非数字数据放入数据类型中。

这一事实的结果是,您无法编写以下内容:

type Num num => [(Char,num)]

因为上下文 (Num num) 没有数据构造函数 T,所以无法要求它;如果我有 [('4',False)],它会自动匹配类型 [(Char,num)],因为它是一个同义词。编译器不能在决定某个类型之前在您的代码中寻找实例。在 data 的情况下,它有一个构造函数 T,告诉它类型,并且可以保证有一个 Num num 实例,因为它检查了您对函数 T 的使用。没有 T,就没有上下文。


感谢您提供详尽但易于理解的解释! - fast-reflexes
1
在你的例子中,你使用 bodybodypart 来表示相同的类型,所以这段代码不应该编译通过。我可以编辑它,但是修改代码来修复问题并不被看好,我更愿意让你选择要使用哪个名称作为类型变量。 - amalloy
@amalloy,谢谢你。我也在num上犯了另一个错误。还有我没有发现的吗? - AndrewC

2

GHC可以使用RankNTypes实现此功能。

因此,您可以这样做:

type NumList = forall a . (Num a ,Fractional a) => [a]

如果我们有以下内容:

numList:: NumList
numList = [1,2,3]

fracList:: NumList
fracList = [1.3,1.7]

进行连接操作的结果是:

fracList ++ numList :: Fractional a => [a]

NumList是一个同义词。 总的来说,我真的不明白这种情况的意义所在。


这个 RankNTypes 看起来很有趣,我可能会稍后研究一下,但是在阅读了你的答案后,我决定不再使用 Haskell 进行此操作,这让我重新考虑了一下... 谢谢! - fast-reflexes

0

当你无法恢复原始类型时,拥有这样的类型并没有太多意义。如果你只需要特定的Num,那么你应该简单地将它们包装起来,而不是施展繁重的魔法:

data NumWrapper = WInt Int 
                | WDouble Double 
                | WFloat Float 
                deriving Show

numList :: [NumWrapper]
numList = [WInt 12, WFloat 1.2, WDouble 3.14]

如果你真的想对任意的Num类型开放,那么列表可能不是你想要的集合。有HLists等等,参见http://www.haskell.org/haskellwiki/Heterogenous_collections


谢谢!我理解这个建议... 这对我来说是可行的,但也会给我带来很多麻烦,所以你指引了我正确的方向,让我知道我之前尝试的方法并不是很好... 谢谢! - fast-reflexes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接