Haskell使用惰性求值来实现递归,因此将任何东西都视为在需要时提供值的承诺(这被称为thunk)。unk子只会尽可能减少,而不会减少。这类似于您数学上简化表达式的方式,因此以这种方式思考将很有帮助。您的代码未指定评估顺序的事实使编译器可以进行很多甚至更聪明的优化,而不仅仅是过去的尾声消除。如果您要优化,请编译-O2
。
让我们看看我们如何评估facSlow 5
案例研究:
facSlow 5
5 * facSlow 4 -- Note that the `5-1` only got evaluated to 4
5 * (4 * facSlow 3) -- because it has to be checked against 1 to see
5 * (4 * (3 * facSlow 2)) -- which definition of `facSlow` to apply.
5 * (4 * (3 * (2 * facSlow 1)))
5 * (4 * (3 * (2 * 1)))
5 * (4 * (3 * 2))
5 * (4 * 6)
5 * 24
120
因此,正如您担心的那样,在进行任何计算之前,我们都会积累一些数字,但是与您担心的情况不同,没有堆栈facSlow
函数挂起等待终止-每次归约都会应用并消失,从而在堆栈中保留一个堆栈框架唤醒(这是因为它(*)
很严格,因此触发了对其第二个参数的求值)。
Haskell的递归函数不是以非常递归的方式求值的!唯一徘徊的呼叫是乘法本身。如果 (*)
将其视为严格的数据构造函数,则这称为保护递归(尽管通常在非严格数据构造函数中称为此类,但在进一步访问的强制下,剩下的是数据构造函数)。
现在让我们看一下尾递归fac 5
:
fac 5
fac' 5 1
fac' 4 {5*1} -- Note that the `5-1` only got evaluated to 4
fac' 3 {4*{5*1}} -- because it has to be checked against 1 to see
fac' 2 {3*{4*{5*1}}} -- which definition of `fac'` to apply.
fac' 1 {2*{3*{4*{5*1}}}}
{2*{3*{4*{5*1}}}} -- the thunk "{...}"
(2*{3*{4*{5*1}}}) -- is retraced
(2*(3*{4*{5*1}})) -- to create
(2*(3*(4*{5*1}))) -- the computation
(2*(3*(4*(5*1)))) -- on the stack
(2*(3*(4*5)))
(2*(3*20))
(2*60)
120
因此,您可以看到尾部递归本身如何并没有节省您任何时间或空间。它不仅要比在总体上采取更多的步骤facSlow 5
,而且还会构建一个嵌套的thunk(在此处显示为{...}
)-需要额外的空间 -它描述了将来的计算以及要执行的嵌套乘法。
这thunk是然后通过遍历揭开它的底部,重新创建堆栈上的计算。对于这两个版本,还有很长的计算时间导致堆栈溢出的危险。
如果我们想手动优化,我们要做的就是使其严格。您可以使用严格的应用运算符$!
来定义
facSlim :: (Integral a) => a -> a
facSlim x = facS' x 1 where
facS' 1 y = y
facS' x y = facS' (x-1) $! (x*y)
这facS'
在第二个论点中要求严格。(它的第一个参数已经很严格了,因为必须对其进行评估,以确定facS'
要应用哪个定义。)
有时严格性可以提供极大帮助,有时则是一个很大的错误,因为懒惰更为有效。这是个好主意:
facSlim 5
facS' 5 1
facS' 4 5
facS' 3 20
facS' 2 60
facS' 1 120
120
我想您想实现的目标是什么。
摘要
- 如果要优化代码,第一步是使用
-O2
- 尾部递归只有在没有堆积的情况下才是好的,并且增加严格性通常可以在适当的情况下防止这种情况的发生。当您生成稍后需要的结果时,会发生这种情况。
- 有时尾部递归是一个不好的计划,而保护性递归则更合适,例如,当您要逐步生成结果时,将需要一点一点地分批进行。见这个问题有关
foldr
,并foldl
举例来说,并测试他们反目成仇。
尝试以下两个:
length $ foldl1 (++) $ replicate 1000
"The size of intermediate expressions is more important than tail recursion."
length $ foldr1 (++) $ replicate 1000
"The number of reductions performed is more important than tail recursion!!!"
foldl1
是尾递归,而foldr1
执行受保护的递归,以便立即提供第一项以供进一步处理/访问。(第一个“括号”立即移到左侧(...((s+s)+s)+...)+s
,迫使其输入列表完全移到末尾,并在需要完整结果之前更快地构建大量的未来计算;第二个括号在右侧逐渐s+(s+(...+(s+s)...))
消耗输入内容一点一点地列出,因此整个过程可以在恒定的空间内进行优化操作)。
您可能需要根据所使用的硬件来调整零的数量。