列表融合无法在此处起作用的问题实际上相当微妙。假设我们定义正确的RULE
来消除列表:
import GHC.Base
sum2 :: Num a => [a] -> a
sum2 = sum
{-# NOINLINE [1] sum2 #-}
{-# RULES "sum" forall (f :: forall b. (a->b->b)->b->b).
sum2 (build f) = f (+) 0 #-}
(简短的解释是,我们将
sum2
定义为
sum
的别名,禁止 GHC 提前内联它,这样
RULE
就有机会在
sum2
被消除之前触发。然后我们直接查找紧挨着列表构建器
build
的
sum2
(参见
定义),并用直接算术运算替换它。)
(这种方法效果有好坏之分,因为它产生了以下 Core 代码:)
Main.$wgo =
\ (w_s1T4 :: GHC.Prim.Int
case GHC.Prim.remInt
__DEFAULT ->
case w_s1T4 of wild_Xg {
__DEFAULT -> Main.$wgo (GHC.Prim.+
15000000 -> 0
};
0 ->
case w_s1T4 of wild_Xg {
__DEFAULT ->
case Main.$wgo (GHC.Prim.+
GHC.Prim.+
};
15000000 -> 15000000
}
}
很好,完全融合的代码 - 唯一的问题在于我们有一个对
$wgo
的调用不在尾递归位置。这意味着我们不是在看一个循环,而实际上是一个深度递归函数,程序结果可预测:
Stack space overflow: current size 8388608 bytes.
这里的根本问题在于Prelude的列表融合只能融合右折叠,而直接使用右折叠计算总和会导致过多的堆栈消耗。
显然的解决方案是使用可以处理左折叠的融合框架,例如Duncan的stream-fusion包,该包实际上实现了sum
融合。
另一个解决方案是通过“hack”来绕过它 - 使用右折叠来实现左折叠:
main = print $ foldr (\x c -> c . (+x)) id [2,4..15000000] 0
实际上,使用当前版本的GHC,这确实可以生成接近完美的代码。另一方面,这通常不是一个好主意,因为它依赖于GHC能够聪明地消除部分应用的函数。即使是在链中加入filter
也会破坏该特定优化。
sum
融合,因此可以尝试使用GHC 7.9(当前的git HEAD)。 - Joachim Breitner