Tail call optimization in Mathematica?

后端 未结 2 1514
青春惊慌失措
青春惊慌失措 2020-12-07 14:48

While formulating an answer to another SO question, I came across some strange behaviour regarding tail recursion in Mathematica.

The Mathematica documentation hints

2条回答
  •  北海茫月
    2020-12-07 15:31

    The idea of this answer is to replace the brackets () by a wrapper that does not make our expressions grow. Note that the function we are finding an alternative for is really CompoundExpression, as the OP was correct in remarking this function was ruining the tail recursion (see also the answer by Leonid). Two solutions are provided. This defines the first wrapper

    SetAttributes[wrapper, HoldRest];
    wrapper[first_, fin_] := fin
    wrapper[first_, rest__] := wrapper[rest]
    

    We then have that

    Clear[f]
    k = 0;
    mmm = 1000;
    f[n_ /; n < mmm] := wrapper[k += n, f[n + 1]];
    f[mmm] := k + mmm
    Block[{$IterationLimit = Infinity}, f[0]]
    

    Correctly calculates Total[Range[1000]].

    ------Note-----

    Note that it would be misleading to set

    wrapper[fin_] := fin;
    

    As in the case

    f[x_]:= wrapper[f[x+1]]
    

    Tail recursion does not occur (because of the fact that wrapper, having HoldRest, will evaluate the singular argument before applying the rule associated with wrapper[fin_]).

    Then again, the definition above for f is not useful, as one could simply write

    f[x_]:= f[x+1]
    

    And have the desired tail recursion.

    ------Another note-----

    In case we supply the wrapper with a lot arguments, it may be slower than necessary. The user may opt to write

    f[x_]:=wrapper[g1;g2;g3;g4;g5;g6;g7  , f[x+1]]
    

    Second wrapper

    The second wrapper feeds its arguments to CompoundExpression and will therefore be faster than the first wrapper if many arguments are provided. This defines the second wrapper.

    SetAttributes[holdLastWrapper, HoldAll]
    holdLastWrapper[fin_] := fin
    holdLastWrapper[other_, fin_] := 
     Function[Null, #2, HoldRest][other, fin]
    holdLastWrapper[others__, fin_] := 
     holdLastWrapper[
      Evaluate[CompoundExpression[others, Unevaluated[Sequence[]]]], fin]
    

    Note: Returning (empty) Sequences might be very useful in recursion in general. See also my answer here

    https://mathematica.stackexchange.com/questions/18949/how-can-i-return-a-sequence

    Note that this function will still work if only one argument is provided, as it has attribute HoldAll rather than HoldRest, so that setting

    f[x]:= holdLastWrapper[f[x+1]]
    

    Will yield a tail recursion (wrapper does not have this behavior).

    Speed comparison

    Let's create a nice long list (actually an expression with Head Hold) of instructions

    nnnn = 1000;
    incrHeld = 
      Prepend[DeleteCases[Hold @@ ConstantArray[Hold[c++], nnnn], 
        Hold, {2, Infinity}, Heads -> True], Unevaluated[c = 0]];
    

    For these instructions, we can compare the performance (and outcome) of our wrappers with CompoundExpression

    holdLastWrapper @@ incrHeld // Timing
    CompoundExpression @@ incrHeld // Timing
    wrapper @@ incrHeld // Timing
    

    --> {{0.000856, 999}, {0.000783, 999}, {0.023752, 999}}

    Conclusion

    The second wrapper is better if you are not exactly sure when tail recursion will happen or how many arguments you will feed to the wrapper. If you are intent on feeding the wrapper 2 arguments, for example in the case where you realize all the second wrapper does is feed to CompoundExpression and you decide to do this yourself, the first wrapper is better.

    -----final note----

    In CompoundExpression[args, Unevaluated[expr]], expr still gets evaluated before CompoundExpression is stripped, so solutions of this type are no use.

提交回复
热议问题