Link between tail recursion and Continuation

Previously, I talked about tail recursion and continuation, but I still feel that I have to add some questions. Gambling entertainment city

Continuation is a very old program structure. Simply put, it is entire default future of a computation, which is a modeling of the program "what to do next, that is, "What else needs to be done" after "Something is completed ". This method can also be reflected in the tail recursive structure.

For example, the traditional recursive definition of the factorial method is as follows:

int FactorialRecursively(int n){ if (n == 0) return 1; return FactorialRecursively(n - 1) * n;}

Obviously, this is not a method of tail recursion. Of course, we can easily convert it to the method of tail recursion called previously mentioned. But now we can "understand" it like this: Every time we calculate the factorial of N, it is actually "first get the factorial of n-1" and then "multiply by N and return ", so our factorialrecursively method can be transformed:

int FactorialRecursively(int n){ return FactorialContinuation(n - 1, r => n * r);}// FactorialContinuation(n, x => x)int FactorialContinuation(int n, Func<int, int> continuation){ ...}

The factorialcontinuation method means to calculate the factorial of N, pass the result to the continuation method, and return the call result ". Therefore, it is easy to conclude that the factorialcontinuation method itself is a recursive call:

public static int FactorialContinuation(int n, Func<int, int> continuation){ return FactorialContinuation(n - 1, r => continuation(n * r));}

The implementation of factorialcontinuation can be expressed as follows: "Calculate the factorial of N, pass the result into the continuation method, and return", that is, "Calculate the factorial of n-1, multiply the result with N and then call the continuation method ". To implement the logic of "multiply the result with N and then call the continuation method", the Code constructs an anonymous method and passes in the factorialcontinuation method again. Of course, we also need to add recursive exit conditions for it:

public static int FactorialContinuation(int n, Func<int, int> continuation){ if (n == 0) return continuation(1); return FactorialContinuation(n - 1, r => continuation(n * r));}

Obviously, factorialcontinuation implements tail recursion. To calculate the factorial of N, we need to call the factorialcontinuation method as follows to calculate the factorial of 10 and return the result directly ":

FactorialContinuation(10, x => x)

Can you better understand the formula for calculating the nth value of the "Fibonacci" series below?

public static int FibonacciContinuation(int n, Func<int, int> continuation){ if (n < 2) return continuation(n); return FibonacciContinuation(n - 1, r1 => FibonacciContinuation(n - 2, r2 => continuation(r1 + r2)));}

In function programming, such call methods form the "Continuation Passing Style (CPS )". Since the lambda expression of C # can easily form an anonymous method, we can also implement this call method in C. You may wonder, why is it so complicated that the calculation of factorial and the "Fibonacci" series can not be converted into a tail recursion form at once? However, would you like to try the following example?

Pre-order traversal is a typical recursive operation. It is assumed that there are the following treenode classes:

public class TreeNode{ public TreeNode(int value, TreeNode left, TreeNode right) { this.Value = value; this.Left = left; this.Right = right; } public int Value { get; private set; } public TreeNode Left { get; private set; } public TreeNode Right { get; private set; }}

So let's traverse the traditional first order:

public static void PreOrderTraversal(TreeNode root){ if (root == null) return; Console.WriteLine(root.Value); PreOrderTraversal(root.Left); PreOrderTraversal(root.Right);}

Can you use the "normal" method to convert it to a tail recursive call? Preordertraversal is called twice, which means that one call cannot be placed at the end. At this time, we need to use the continuation:

public static void PreOrderTraversal(TreeNode root, Action<TreeNode> continuation){ if (root == null) { continuation(null); return; } Console.WriteLine(root.Value); PreOrderTraversal(root.Left, left => PreOrderTraversal(root.Right, right => continuation(right)));}

Now we use every recursive call as the last operation of the code, and wrap the next operation with continuation. This achieves tail recursion and avoids the accumulation of stack data. It can be seen that although using continuation is a slightly "weird" method of use, it is also essential to use it in some cases.

Improvement on continuation

Look at the implementation of the first-order traversal. Have you found something strange?

PreOrderTraversal(root.Left, left => PreOrderTraversal(root.Right, right => continuation(right)));

In the last step, we construct an anonymous function as the continuation of the second preordertraversal call, but it directly calls the continuation parameter internally. Why don't we directly give it to the second call? As follows:

PreOrderTraversal(root.Left, left => PreOrderTraversal(root.Right, continuation));

We use continuation to implement tail recursion. In fact, the information originally allocated on the stack is lost to the hosting stack. Each anonymous method is actually a hosting object on the stack. Although short-lived objects do not cause much problems to memory resources, they should be minimized, it is certainly helpful for performance. Here is a more obvious example to calculate the size of a binary tree ):

public static int GetSize(TreeNode root, Func<int, int> continuation){ if (root == null) return continuation(0); return GetSize(root.Left, leftSize => GetSize(root.Right, rightSize => continuation(leftSize + rightSize + 1)));}

The getsize method uses the continuation method. It can be understood as "Get the root size, pass the result into the continuation, and return the call result ". We can rewrite it to reduce the constructor times of the continuation method:

public static int GetSize2(TreeNode root, int acc, Func<int, int> continuation){ if (root == null) return continuation(acc); return GetSize2(root.Left, acc, accLeftSize => GetSize2(root.Right, accLeftSize + 1, continuation));}

The getsize2 method has an additional accumulator parameter, and its understanding has also changed: "add the root size to ACC, and then pass the result to continuation, and return the call result ". That is to say, getsize2 returns an accumulated value instead of the actual size of the Root parameter. Of course, when we call getsize2, we only need to set the accumulators to zero:

GetSize2(root, 0, x => x)

Summary

In imperative programming, we can often use loops instead of recursion to solve some problems, so as not to cause stack overflow due to data size. However, in functional programming, the only method to implement a "loop" is "recursion". Therefore, tail recursion and CPS are of great significance for functional programming. In Functional Languages, the introduction of continuation is a very natural process. In fact, any program can be converted to the continuation structure through the so-called CPS (continuation Passing Style) transformation. Understanding tail recursion is also very helpful for programming thinking. Therefore, you may wish to think more and practice it for your own use.

Add some continuation knowledge