8

假设我想为所有带有 Int 的列表创建一个类型同义词。

我可以做:

type NumberList = [Int]

但是如果我想调用所有包含数字 NumberList 的列表呢?我如何设置约束并说只要“Num a”的所有[a]都应该被称为相同的?

编辑:: 看到我重新思考的答案后。似乎我违背了 Haskell 背后的一个基本理念,而且回报相对较小(只是形式上的问题)。我决定这样做:如果一个类型需要两个彼此相同但仅在 Int 或 Float 方面不同的实例,那么它们之间的差异太小,无法保证完成使用 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)
4

4 回答 4

10

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

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

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

现在,您将在箭头左侧使用 NumberList 的大多数类型,其中 «exists» 有效地将其含义更改为 «forall»。

也就是说,而不是写

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)

但是现在原始列表类型和 sum 类型之间的联系丢失了——至少对于 Haskell 的类型系统来说是这样。如果您随后决定编写一个函数,例如

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

那么你会得到一个类型错误,因为sum ys可能是任何类型,不一定是整数类型。

如果你把所有东西都推到极致,让每一种类型都存在量化,那它会起作用——但是你最终会基本上用另一种类似面向对象的语言来解决它们的所有问题。

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

于 2012-10-03T23:25:11.600 回答
6

使用数据和参数,而不是existentials来获取上下文

我想如果你想要

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 构造函数或通过 GADT 使用存在类型,因为将num参数添加到您的数据类型在实践中非常有用,即使它需要更多的输入。

类型同义词的限制?

请注意,当您使用类似的约束时

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

实际上只是将构造函数的类型更改T

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

而不是T :: [(Char,num)] -> Thing num. 这意味着每次您使用T时,都需要有一个 context (Num num),但这正是您想要的 - 阻止人们将数据放入您的非数字数据类型中。

这个事实的结果是你不能

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

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

于 2012-10-04T06:13:38.307 回答
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 是同义词。总而言之,在这种情况下,我真的不明白这一点。

于 2012-10-03T23:52:22.283 回答
0

当您无法恢复原始类型时,拥有这样的类型没有多大意义。如果你只需要某些Nums,你应该简单地包装它们而不是变出沉重的魔法:

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

于 2012-10-04T06:39:01.420 回答