通过给出明确的类型签名,您可以防止 GHC 对您的代码做出某些假设。我将展示一个示例(取自这个问题):
foo (x:y:_) = x == y
foo [_] = foo []
foo [] = False
根据 GHCi,此函数的类型是Eq a => [a] -> Bool
,正如您所期望的。但是,如果您foo
使用此签名声明,您将收到“模糊类型变量”错误。
这个函数只能在没有类型签名的情况下工作的原因是因为类型检查在 GHC 中是如何工作的。当您省略类型签名时,foo
假定[a] -> Bool
某些固定类型具有单型a
。一旦你完成了绑定组的输入,你就可以概括这些类型。那就是你得到forall a. ...
.
另一方面,当您声明多态类型签名时,您明确声明它foo
是多态的(因此类型[]
不必与第一个参数的类型匹配)并且繁荣,您会得到模棱两可的类型变量。
现在,知道了这一点,让我们比较一下核心:
fib = 0:1:zipWith (+) fib (tail fib)
-----
fib :: forall a. Num a => [a]
[GblId, Arity=1]
fib =
\ (@ a) ($dNum :: Num a) ->
letrec {
fib1 [Occ=LoopBreaker] :: [a]
[LclId]
fib1 =
break<3>()
: @ a
(fromInteger @ a $dNum (__integer 0))
(break<2>()
: @ a
(fromInteger @ a $dNum (__integer 1))
(break<1>()
zipWith
@ a @ a @ a (+ @ a $dNum) fib1 (break<0>() tail @ a fib1))); } in
fib1
对于第二个:
fib :: Num a => [a]
fib = 0:1:zipWith (+) fib (tail fib)
-----
Rec {
fib [Occ=LoopBreaker] :: forall a. Num a => [a]
[GblId, Arity=1]
fib =
\ (@ a) ($dNum :: Num a) ->
break<3>()
: @ a
(fromInteger @ a $dNum (__integer 0))
(break<2>()
: @ a
(fromInteger @ a $dNum (__integer 1))
(break<1>()
zipWith
@ a
@ a
@ a
(+ @ a $dNum)
(fib @ a $dNum)
(break<0>() tail @ a (fib @ a $dNum))))
end Rec }
与foo
上面一样,使用显式类型签名,GHC 必须将fib
其视为潜在的多态递归值。我们可以将一些不同的Num
字典传递给fib
in zipWith (+) fib ...
,此时我们将不得不丢弃大部分列表,因为不同Num
意味着不同(+)
。当然,一旦您使用优化进行编译,GHC 会注意到Num
字典在“递归调用”期间永远不会更改,并将其优化掉。
在上面的核心中,您可以看到 GHC 确实一次又一次地给出fib
了一个Num
字典(名为)。$dNum
因为fib
在整个绑定组的泛化完成之前假设没有类型签名是单态的,所以fib
子部分被赋予与整体完全相同的类型fib
。多亏了这一点,fib
看起来像:
{-# LANGUAGE ScopedTypeVariables #-}
fib :: forall a. Num a => [a]
fib = fib'
where
fib' :: [a]
fib' = 0:1:zipWith (+) fib' (tail fib')
而且由于类型保持固定,您可以只使用开始时给出的一个字典。