当您编写fibsA :: Num a => [a] 时,编译器会构造本质上的内容
fibsA :: NumDict a -> [a]
在哪里
data NumDict a = NumDict
{ (+) :: a -> a -> a
, (-) :: a -> a -> a
, (*) :: a -> a -> a
, negate :: a -> a
, abs :: a -> a
, signum :: a -> a
, fromInteger :: Integer -> a
}
请注意,Num a 已从约束转变为函数的参数。 A typeclass is essentially just a lookup table for each type that implements the class。所以对于Num,默认情况下你会拥有
mkInteger_NumDict :: NumDict Integer
mkInteger_NumDict = NumDict
{ (+) = integer_plus
, (-) = integer_minus
, (*) = integer_mult
, ...
}
mkInt_NumDict :: NumDict Int
mkFloat_NumDict :: NumDict Float
mkDouble_NumDict :: NumDict Double
当实例被解析时,这些会自动传递给使用类型类的函数。这意味着我们的函数fibsA 本质上接受了一个参数。当你从 GHCi 调用它时,默认规则会启动并选择 Integer,但由于它是这样调用的,它在内部看起来更像这样:
{-# RecordWildCards #-} -- To reduce typing
fibsA :: NumDict a -> [a]
fibsA nd@(NumDict{..}) = fromInteger 0 : fromInteger 1 : zipWith (+) (fibsA nd) (tail $ fibsA nd)
你看到这个问题了吗?它仍然是递归的,但现在它必须在每一步都进行函数调用,从而降低了性能。如果你想让它变得非常快,聪明的程序员会做的
fibsA nd@(NumDict{..}) = fromInteger 0 : fromInteger 1 : zipWith (+) fibsA' (tail fibsA')
where fibsA' = fibsA nd
这至少允许记忆。然而,haskell 二进制文件并不能真正在运行时执行这种优化,这发生在编译时。所以你最终得到的是一个较慢的递归函数。使用fibsB,您可以具体指定类型,它的类型签名没有多态约束。值fibsB 没有隐式或显式参数,因此当引用它时,它是指向内存中同一对象的指针。 fibsA 是指向函数的指针,因此当递归使用时,它会返回内存中的新对象,并且没有记忆。这就是为什么fibsB 比fibsA 快的原因,只有fibsB 得到优化,因为编译器不必使其适用于所有Num,仅适用于Integer。