我可以总结一下我个人经验得出的结论,并声明以下内容可能不是完全正确的解释。答案似乎在于 Mathematica 调用堆栈与传统调用堆栈之间的差异,这源于 Mathematica 模式定义的函数是真正的规则。因此,没有真正的函数调用。Mathematica 需要一个堆栈的原因不同:因为正常的计算是从表达式树的底部开始的,它必须保留中间表达式,以防(子)表达式的更深的部分由于规则应用而被替换(某些部分一个表达式从底部增长)。尤其是对于定义我们在其他语言中称为非尾递归函数的规则时,情况尤其如此。所以,再一次,
这意味着,如果作为规则应用的结果,一个(子)表达式可以被整体重写,则表达式分支不需要保留在表达式堆栈上。这可能是 Mathematica 中所谓的尾调用优化——这就是为什么在这种情况下我们使用迭代而不是递归(这是规则应用程序和函数调用之间差异的一个很好的例子)。像这样的规则f[x_]:=f[x+1]
就是这种类型。但是,如果某些子表达式被重写,产生更多的表达式结构,那么表达式必须存储在堆栈中。规则f[x_ /; x < 5] := (n += 1; f[x + 1])
属于这种类型,在我们回忆()
起CompoundExpression[]
. 在这里发生的示意图是f[1] -> CompoundExpression[n+=1, f[2]] -> CompoundExpression[n+=1,CompoundExpression[n+=1,f[3]]]->etc
. 即使对 f 的调用每次都是最后一次,它也发生在完整的CompoundExpression[]
执行,所以它仍然必须保存在表达式堆栈中。有人可能会争辩说,这是一个可以进行优化的地方,为 CompoundExpression 设置一个例外,但这可能并不容易实现。
现在,为了说明我上面示意性描述的堆栈累积过程,让我们限制递归调用的数量:
Clear[n, f, ff, fff];
n = 0;
f[x_ /; x < 5] := (n += 1; f[x + 1]);
ff[x_] := Null /; (n += 1; False);
ff[x_ /; x < 5] := ff[x + 1];
fff[x_ /; x < 5] := ce[n += 1, fff[x + 1]];
跟踪评估:
In[57]:= Trace[f[1],f]
Out[57]= {f[1],n+=1;f[1+1],{f[2],n+=1;f[2+1],{f[3],n+=1;f[3+1],{f[4],n+=1;f[4+1]}}}}
In[58]:= Trace[ff[1],ff]
Out[58]= {ff[1],ff[1+1],ff[2],ff[2+1],ff[3],ff[3+1],ff[4],ff[4+1],ff[5]}
In[59]:= Trace[fff[1],fff]
Out[59]= {fff[1],ce[n+=1,fff[1+1]],{fff[2],ce[n+=1,fff[2+1]],{fff[3],ce[n+=1,fff[3+1]],
{fff[4],ce[n+=1,fff[4+1]]}}}}
从中可以看出,表达式堆栈为f
and累积fff
(后者仅用于表明这是一种通用机制,ce[]
只是带有一些任意头部),但不是 for ff
,因为出于模式匹配的目的,第一个定义 forff
是一个尝试但不匹配的规则,第二个定义ff[arg_]
完全重写,不会生成需要进一步重写的更深的子部分。所以,底线似乎你应该分析你的函数,看看它的递归调用是否会从底部增长评估的表达式。如果是,就 Mathematica 而言,它不是尾递归的。
如果不展示如何手动进行尾调用优化,我的回答将是不完整的。例如,让我们考虑 Select 的递归实现。我们将与 Mathematica 链表一起工作,使其相当高效,而不是一个玩具。下面是非尾递归实现的代码:
Clear[toLinkedList, test, selrecBad, sel, selrec, selTR]
toLinkedList[x_List] := Fold[{#2, #1} &, {}, Reverse[x]];
selrecBad[fst_?test, rest_List] := {fst,If[rest === {}, {}, selrecBad @@ rest]};
selrecBad[fst_, rest_List] := If[rest === {}, {}, selrecBad @@ rest];
sel[x_List, testF_] := Block[{test = testF}, Flatten[selrecBad @@ toLinkedList[x]]]
我使用 Block 和 selrecBad 的原因是为了更容易使用 Trace。现在,这会破坏我机器上的堆栈:
Block[{$RecursionLimit = Infinity}, sel[Range[300000], EvenQ]] // Short // Timing
您可以跟踪小列表以了解原因:
In[7]:= Trace[sel[Range[5],OddQ],selrecBad]
Out[7]= {{{selrecBad[1,{2,{3,{4,{5,{}}}}}],{1,If[{2,{3,{4,{5,{}}}}}==={},{},selrecBad@@{2,{3,{4,
{5,{}}}}}]},{selrecBad[2,{3,{4,{5,{}}}}],If[{3,{4,{5,{}}}}==={},{},selrecBad@@{3,{4,{5,
{}}}}],selrecBad[3,{4,{5,{}}}],{3,If[{4,{5,{}}}==={},{},selrecBad@@{4,{5,{}}}]},{selrecBad[4,
{5,{}}],If[{5,{}}==={},{},selrecBad@@{5,{}}],selrecBad[5,{}],{5,If[{}==={},{},selrecBad@@{}]}}}}}}
发生的情况是结果在列表中越来越深。解决方案是不增加结果表达式的深度,实现这一目标的一种方法是让 selrecBad 接受一个额外的参数,即累积结果的(链接)列表:
selrec[{fst_?test, rest_List}, accum_List] :=
If[rest === {}, {accum, fst}, selrec[rest, {accum, fst}]];
selrec[{fst_, rest_List}, accum_List] :=
If[rest === {}, accum, selrec[rest, accum]]
并相应修改主函数:
selTR[x_List, testF_] := Block[{test = testF}, Flatten[selrec[toLinkedList[x], {}]]]
这将很好地通过我们的功率测试:
In[14]:= Block[{$IterationLimit= Infinity},selTR[Range[300000],EvenQ]]//Short//Timing
Out[14]= {0.813,{2,4,6,8,10,12,14,16,18,20,
<<149981>>,299984,299986,299988,299990,299992,299994,299996,299998,300000}}
(请注意,这里我们必须修改 $IterationLimit,这是一个好兆头)。使用 Trace 揭示了原因:
In[15]:= Trace[selTR[Range[5],OddQ],selrec]
Out[15]= {{{selrec[{1,{2,{3,{4,{5,{}}}}}},{}],If[{2,{3,{4,{5,{}}}}}==={},{{},1},selrec[{2,{3,{4,
{5,{}}}}},{{},1}]],selrec[{2,{3,{4,{5,{}}}}},{{},1}],If[{3,{4,{5,{}}}}==={},{{},1},selrec[{3,
{4,{5,{}}}},{{},1}]],selrec[{3,{4,{5,{}}}},{{},1}],If[{4,{5,{}}}==={},{{{},1},3},selrec[{4,
{5,{}}},{{{},1},3}]],selrec[{4,{5,{}}},{{{},1},3}],If[{5,{}}==={},{{{},1},3},selrec[{5,
{}},{{{},1},3}]],selrec[{5,{}},{{{},1},3}],If[{}==={},{{{{},1},3},5},selrec[{},{{{{},1},3},5}]]}}}
也就是说,此版本不累积中间表达式的深度,因为结果保存在单独的列表中。