让我们从
开始boom :: Int -> Maybe a -> Maybe a
boom 0 x = x
boom n x = boom (n-1) (x >>= (\y -> Just y))
这是一个简单的函数,只是将>>=
Maybe
值重复推入一个简单的\y -> Just y
函数。
现在,程序
main = do
let z = boom 10 (Nothing :: Maybe Int)
putStrLn $ show z
一瞬间跑得很快。但是,程序
main = do
let z = boom 10000000 (Nothing :: Maybe Int)
putStrLn $ show z
需要几秒钟才能完成,即使我使用ghc -O
进行编译(GHC 7.8.3)。
这意味着Haskell无法对此进行优化。 Nothing
被反复推入一个函数,即使它没有必要这样做。
我的问题是,为什么?为什么不能推断Nothing
在重复推特中总是以Nothing
结尾?换句话说,为什么它不能在第一个Nothing
时立即短路?
答案 0 :(得分:7)
你的一个很好的例子是一个慢的函数,因为它是尾递归的。在严格的语言中,尾递归函数通常是首选,因为它们通常会带来更好的性能(在时间和空间上)。在懒惰的尾部递归并不是那么有益。实际上,函数的非尾递归变体是:
boom :: Int -> Maybe a -> Maybe a
boom 0 x = x
boom n x = x >>= (\y -> boom (n-1) (Just y))
当n
为x
时,上述内容仍会循环Just something
次。但是,它将在常量空间中执行此操作,而不像在第二个参数中构建大型thunk的原始代码。更好的是,当x
为Nothing
时,上述代码将立即返回。
我确实意识到这并没有真正回答你关于“为什么”GHC无法对此进行优化的问题。但希望,它可以表明这些优化非常微妙,并且通常涉及归纳推理。期望编译器对此进行优化可能有点过分。
答案 1 :(得分:1)
您需要使用-fforce-recomp
使用ghc
编译并获得1.44秒
使用ghc -O
编译并获得1.44秒
使用ghc -O -fforce-recomp
编译并获得0.00s到0.04s
请注意
它仍无法与boom maxBound (Nothing :: Maybe Int)
你会等很长时间。
答案 2 :(得分:0)
因为boom
首先展开了n-1
。
所以boom 100 x
= boom (100-1) ...
= boom (100-1-1) ...
等...
你可以尝试交换繁荣的论点(但我不确定会改变什么)。