Haskell中的类型签名

3
我的问题与类型签名有关。
以下代码编译通过:
data Vector a = Vector a a a deriving (Show) 
vMult :: (Num a) => Vector a -> a -> Vector a 
(Vector i j k) `vMult` m = Vector (i*m) (j*m) (k*m)

然而,我不明白为什么将上述类型签名(第2行)替换为以下内容不起作用

vMult :: (Num a) => Vector a -> Num -> Vector a  

我的理解是,由于m的类型为Num(例如数字8),而i,j,k也是Num类型,因此计算Vector (i*m) (j*m) (k*m)不应该有问题。
请核对我的理解是否正确。

11
Num 不是一个类型,而是一个类型类。理解这两者之间的区别是理解为什么第二个签名无效的关键。 - Thomas M. DuBuisson
明白了,我漏掉了那个点。非常感谢你,Thomas! - labrynth
3个回答

7

Num a实际上根本不是一种类型

Num是一个类型类,因此如果我说Num a,它意味着a是一个数字类型,因此

vMult :: (Num a) => Vector a -> a -> Vector a

意思是“只要 a 是数字类型,vMult 就接受一个由 a 组成的向量和一个单独的 a,并返回一个由 a 组成的向量。”

这意味着 vMult 可以像这样工作:
vMult :: Vector Int -> Int -> Vector Int 或者
vMult :: Vector Double -> Double -> Vector Double
请注意,你可以通过将原始函数中的所有 a 替换为单个数字类型来得到每个类型。

Num 单独使用没有意义

Num 单独使用的意思是“是数字类型”,因此如果你的函数具有以下类型签名:

vMult :: (Num a) => Vector a -> Num -> Vector a

如果要翻译成英文,应该是这样的:“只要a是数字类型,vMult就接受一个由a组成的向量和一个数字类型,并返回一个由a组成的向量。” 这在英语中与Haskell一样是不合文法的错误。这就像说“给我一些黄油,还有一个有锋利边缘的东西”而不是“给我一些黄油和一把刀”。可行的类型标记就像是说“找到一个可以涂抹的东西(称之为k),然后给我一些黄油和k”。
同时,你也不能使用“Num a”代替“a”,比如“Vector a -> Num a -> Vector a”,因为你不能在名词的地方放置一个断言:“给我你的钥匙链和一个钥匙都是金属做的,这样我就可以给你一个新的钥匙链”。

2

乘法在Num类型类中定义,作用于两个相同类型的参数Num aNum b

(*) :: (Num a) => a -> a -> a 

如果您的类型签名中只有Num,那么这就相当于要求允许在一个IntegerFloat之间进行乘法运算,因为两者都属于类型类Num。这就是为什么您需要指定应该使用的类型而不仅仅是Typeclass的原因。
当您将a用于Vector时,已经有一个约束条件确保a应属于Num。为了将所选标量乘以Vector中的每个元素,该标量不仅必须属于Num,而且必须是相同的类型,即a

我明白了。谢谢你的回答,Shree。 - labrynth
3
我不明白它类似于混合不同类型的意义在哪里。你是指必须有一个“a”才能表达所有类型相同的意思吗?这是正确的,但如果你在“Num”之后放置一个“a”,仍然不能使用该签名。 - AndrewC
@AndrewC 我意识到我之前的回答有误导性。我已经编辑过了,使其更加合理。感谢你指出其中的缺陷! - shree.pat18

2
"Num"不是一个可以在那里使用的具体类型,而是一个一元类型构造函数,必须使用另一个类型进行特化才能使用。用面向对象的术语来说,"Num"不是一个类似于抽象的超类类型,需要从中派生,而是一个必须针对其使用进行特化的工厂。
因此,试图将"Num"传递到需要kind(类型的类型)*的位置就像尝试传递一个函数(:: a -> b),其中需要一个值(:: c)一样(只是前者在类型级别上完成)。
ghci> :kind Int
Int :: *

ghci> :kind Num
Num :: * -> Constraint

因此,我们可以看到,在类型检查时将Num应用于任何种类的*类型会产生Constraint种类,并且Constraint只能在类型签名中的=>之前使用(限制在Vector a -> a -> Vector a中的a位置上正确的类型,这就是Constraint的含义)。

4
从技术上讲,“Num”是一个类型构造器是正确的,但是这样说有点令人困惑,因为它并未构造出实际的Haskell类型(如果是这样的话,它将具有“* -> ”的种类而不是“ -> Constraint”)。 - leftaroundabout
@leftaroundabout 是的,我已经编辑掉了我最初的误解,这种Num* -> * - Dmytro Sirenko

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