通过给出明确的类型签名,您可以防止 GHC 对您的代码做出某些假设。我将展示一个示例(取自question):
foo (x:y:_) = x == y
foo [_] = foo []
foo [] = False
根据 GHCi,此函数的类型是 Eq a => [a] -> Bool,正如您所期望的那样。但是,如果您使用此签名声明 foo,您将收到“模糊类型变量”错误。
这个函数只能在没有类型签名的情况下工作的原因是因为类型检查在 GHC 中是如何工作的。当您省略类型签名时,foo 被假定为具有某些固定类型 a 的单型 [a] -> Bool。一旦你完成了绑定组的输入,你就可以概括这些类型。这就是您获得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 字典传递给zipWith (+) fib ... 中的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')
而且由于类型保持固定,您可以只使用一开始给出的字典。