带有Integral类型类的Haskell斐波纳契列表

时间:2013-10-01 12:25:11

标签: haskell typeclass fibonacci

如果我在GHCi中加载以下列表,列表会慢慢计算,直到程序最终在计算3524578(列表中的第33项)之后关闭。

fibonacci :: (Integral a) => [a]
fibonacci = 0 : 1 : zipWith (+) fibonacci (tail fibonacci)

如果我删除第一行并加载以下内容,则会非常快速地计算列表并且GHCi不会关闭。

fibonacci = 0 : 1 : zipWith (+) fibonacci (tail fibonacci)

为什么多态列表比Integer列表慢得多?为什么GHCi会关闭?

2 个答案:

答案 0 :(得分:3)

使用你的第一个版本,它看起来像一个值,你实际上写了一个函数(一个比Haskell本身更低级别的函数)。

要理解这一点,请考虑以下可能与您的定义相关的代码:

main = println (fibonacci !! 17 :: Int, fibonacci !! 19 :: Integer)

当然,17和19完全是武断的。关键是第一个元组元素需要将斐波纳契计算为Int的列表,而第二个元素假设它是Integers的列表。您可能知道,Haskell中没有这样的列表,只有IntInteger - 列表是[Int][Integer](或完全不同的东西)。< / p>

因为事实如此,我们可以在纯粹的逻辑基础上得出结论,在没有深入了解Haskell运行时系统的情况下,fibonacci 既不也不是Int的列表。 Integer的列表,也不包含其他内容的列表 - 这只是根据请求构建此类列表的一种方法。

话虽如此,只要你这样做:

take 10 fibonacci

它应该没那么重要(斐波纳契只计算一次最多10个元素)。

但是当你说

map (fibonacci !!) [1..10]

可能会为每个索引重新计算斐波纳契。显然,索引越高,所需的时间就越长。

答案 1 :(得分:1)

这似乎很可疑,所以我做了一些调查。 首先我做了两个模块:

-- fib0.hs
fibonacci :: (Integral a) => [a]
fibonacci = 0 : 1 : zipWith (+) fibonacci (tail fibonacci)
main = print $ take 10000 $ fibonacci

-- fib1.hs
main = print $ take 10000 $ fibonacci
fibonacci = 0 : 1 : zipWith (+) fibonacci (tail fibonacci)

然后编译如下:ghc -O2 -prof -auto-all fib0.hs & ghc -O2 -prof -auto-all fib1.hs(注意:&适用于Windows,* nix使用;分隔多行上的命令)。并使用fib0 +RTS -p & fib1 +RTS -p运行它们。这将运行两个fib和标志+RTS -p生成一个文件名fib0 / 1.prof,其中包含有关您的程序的运行时信息。这样做,我看到他们都花了相同的时间! (确切地说是0.70秒 - 你的机器上可能需要更长的时间,如果你不能等待那么久,那么减少10000个元素。)

你会注意到我用-O2编译了。这将优化设置为“级别”2.有三个级别 - 0,1和2. GHCi默认使用O0。所以我尝试ghc -O0 -prof -auto-all fib0.hs & ghc -O0 -prof -auto-all fib1.hs(ghc默认为-O1,因此您必须手动设置级别0)。难道你不知道吗,当我运行它们时,fib0因内存异常而崩溃而且fib0完成得很好(albiet非常缓慢。我不得不将take 10000减少到take 100 - 但这显然是被期望)。

这将是一种正常行为。优化可以消除糟糕的编程错误 - 比如在使用单一类型时手动执行多态 - 除非必须这样做,否则不要这样做!

如果您想了解更多信息,可以尝试ghc -O0 -f-ext-core fib0.hs & ghc -O0 -f-ext-core fib1.hs。这将为两个程序生成“核心”文件。在开始制作目标文件之前,Core是GHC编译的最后一个阶段。它生成纯文本.hcr文件。这些可能非常难以阅读,所以我要强调一些关键点。

fib0.hcr包含以下内容:

...
base:GHCziNum.fromInteger @ ac zddNumazzzz
...

基本上,您在序列中的每个数字上调用fromInteger,从0和1开始。为什么这样?你告诉它,“斐波纳契的类型必须是任何数字类型”。它创建0和1作为Integer,然后使用fromInteger创建类型Num a => a的值(这是创建多态文字的唯一方法)。所有这些对fromInteger的调用都会构建非常昂贵的thunk - 所以你会遇到内存异常。

看一下fib1.hcr。它没有包含fromInteger。让GHC推断出斐波纳契的类型,只需使用Integer,这意味着没有任何风暴。

为什么优化会消除此问题? GHC注意到您只使用斐波纳契来打印数字。您不需要这种多态性。所以它优化了它! 请注意,它还可以做其他一些使fibonacci更快,更快,如内联的东西。