Recursive implementations tend to be clearer than loop-based implementations, but recursive implementations can incur additional overhead due to the need to allocate and manage stack frames each time the method calls, which results in a slow recursive implementation and the possibility of running out of stack space (that is, stack overflow) quickly.
To avoid stack overflow, a recommended practice is to rewrite the program into tail recursion to take advantage of some of the compiler's tail-recursive optimizations to avoid overflow.
But we will not only think, what is the difference between normal recursion and tail recursion? What is the end-recursive optimization of the compiler doing?
The difference between tail recursion and normal recursion is that the return value of the tail recursive function is a simple recursive call with no additional operations. The actual operation is done by passing an accumulator variable all the way to the subsequent call until the recursion is complete.
The definition above may be less understood, and the next section will have an example to provide a clearer explanation. The only thing you need to know now is that there is a special recursion that can be optimized by the compiler into a more efficient loop-based implementation that is not affected by the size of the stack.
In Swift, however, we cannot expect the compiler to perform tail-recursive optimizations in all cases.
The flaw has been discussed before in Natasha's blog, and some work has been done and a proposal has been submitted. The proposal focuses on adding properties to make the behavior of the optimizer more verifiable and allowing for explicitly specifying which tail recursion can be optimized (an exception should be thrown if it is not optimized). )
In this article we will explain how to use the trampoline (trampolines) mechanism to solve the shortcomings of the recursive optimization of Swift tail, and give some recursive alternatives.
Translator's Note: Why is it called a trampoline? It is because the trampoline mechanism is essentially a recursive call into a circular call. Recursion will first push the stack continuously (recursive call), and then return the stack continuously. On the trampoline, the stack is pushed once every time the function is called (function call), and then popped out (function return). Repeat this process. If you compare the stack to a trampoline, the process is like jumping on a trampoline. Falling is pushing the stack, popping is popping the stack. process alternately.
You can get to GitHub or here for the playground file that this article uses
trigonometric numbers with recursive calculation
Let's look at an algorithm that calculates the nth number of triangles in a recursive way:
func tri(n:Int)->Int{
if n <= 0 {
return 0
}
return n+tri(n-1)
}
tri(300) //45150
In this simple recursive example, the result of the execution of the recursive invocation and the addition of the parameters are the results. Our initialtri(300)result is that all such numbers are summed in a recursive chain.
To change the above code to a tail-recursive form, we add an accumulator variable to pass the accumulated value to the next layer call.
func ttri(n:Int, acc:Int=0)->Int {
if n<1 {
return acc
}
return ttri(n-1,acc:acc+n)
}
ttri(300) //45150
Notice how the result of the above algorithm is implemented by the accumulator, and the final step is to simply return the accumulated value to complete the entire calculation process.
However, when the input parameters are large, the above two schemes will be crash. Let's look at how the trampoline algorithm can be used to solve this problem.
Jumping bed
The principle behind the trampoline is actually very simple.
The basic definition of a trampoline is a loop that executes a function that either returns a function for the next execution (in thunk or "continuous" form, specifically a data structure that contains the information necessary for a method call), or a value that returns a different type ( In this example, it is the cumulative value) to identify the end of the iteration.
If we are going to use a trampoline to execute our tail recursive function sequentially, we need to make some simple modifications to it in the form of continuous transfer (continuation-passing style).
Update
As Oisdk says, the function we have modified below is just a little bit like the real CPS (the continuous transfer form mentioned above).
In this case, the closure allows you to implement a pseudo-tail recursive optimization by simulating lazy Evaluation. In a continuous transfer form, you pass "continuous" to the recursive function as an extra parameter to the function, and "continuous" defines what to do after the function body finishes executing (in essence, "continuous" is also a function). Simply put, execute the function body first, then execute the "continuous" part, usually at the very beginning, you pass in a meta function. This mechanism allows you to transform ordinary recursive functions into tail recursive functions. But obviously, Swift does not guarantee tail recursion optimization, so this mechanism is useless.
Let's just ignore that. The following is the CPS form for trigonometric number calculations:
Func Tricont (N:int, cont:int-int), int {
return n <= 1? Cont (1): Tricont (n-1) {R in cont (R+N)}
}
Func id< A> (x:a), A {return x}
Tricont (Ten, Cont:id)//55
Thank you for the great explanation.
Instead of directly executing the recursive call, ourttrifunction will return an object that encapsulates the real call , and once we reach the point where execution should end, we will return a sentinel value containing the cumulative result to identify the end of execution.
We define anResultenumeration to represent the value that our modified recursive function might return: The.Donerecursive execution is complete, and it contains the last accumulated value..Callwill contain closures for the method to be executed next.
enum Result<A>{
case Done(A)
case Call(()->Result<A>)
}
Now we can define new functions, including a modified versionttriand some code to implement the trampoline mechanism. The last part is generally placed in a separate function. But in this case put all together, in order to be more readable:
func tritr(n:Int)->Int {
func ttri(n:Int, acc:Int=0)->Result<Int> {
if n<1 {
return .Done(acc)
}
return .Call({
()->Result<Int> in
return ttri(n-1,acc: acc+n)
})
}
// Trampoline section
let acc = 0
var res = ttri(n,acc:acc)
while true {
switch res {
case let .Done(accu):
return accu
case let .Call(f):
res = f()
}
}
}
tritr(300)
Think about the above steps, the realization of the trampoline part is not difficult to understand.
After the initial callttrimethod starts the trampoline, the.Callfunctions contained in the enumeration are executed sequentially, and the accumulated values are updated in each step:
return .Call({
()->Result<Int> in
return ttri(n-1,acc: acc+n)
})
Although the code is different, the behavior is still the same as the recursive version we started with.
Once the calculation is complete,ttrithe function returns an enumeration containing the final result.Done.
Although this implementation is slower than the initial version, all code needs to operate the trampoline. But this version has solved the biggest problem of stack overflow, we can now calculate the number of triangles of any size until the limit of the integer is exceeded.
suggested that.ttrithe implementation of a function can be simplified by a fast-forgetting property modifier@autoclosure.
func call<A>(@autoclosure(escaping) c: () -> Result<A>) -> Result<A> {
return .Call(c)
}
func ttri(n: Int, acc:Int=1) -> Result<Int> {
return n <= 1 ? .Done(acc) : call(tri(n-1, acc: acc+n))
}
Before we go on, let us say a few more examples. It's not a good habit to wrap the code inwhile true, and a better loop check should look like this:
while case .Call(_) = res {
switch res {
case let .Done(accu):
return accu
case let .Call(f):
res = f()
}
}
if case let .Done(ac) = res {
return ac
}
return -1
Of course there is a better way, because we use enumerations to correlate values, we should implement a comparison operator for that enumeration and check for completion at the beginning of the loop.
Now that the basic principle of trampoline has been explained, we can now construct a general function to implement: given a.Resultfunction that returns an enumeration, returns a closed package to execute the original function in the trampoline. Use this function to encapsulate the execution details.
func withTrampoline<V,A>(f:(V,A)->Result<A>) -> ((V,A)->A){
return { (value:V,accumulator:A)->A in
var res = f(value,accumulator)
while true {
switch res {
case let .Done(accu):
return accu
case let .Call(f):
res = f()
}
}
}
}
The main body of the closure we return is basically our trampoline section in the previous example, and thewithTrampolinefunction receives a type as a(V,A)->Result<A>function parameter. We have implemented this function before. A little bit different from the previous version is that we cannot initialize the generic accumulatorAbecause we do not know its specific type, so we expose it to the parameters of the function we return, which is a small flaw.
Here's a look at the general function we just defined:
var fin: (n:Int, a:Int) -> Result<Int> = {_,_ in .Done(0)}
fin = { (n:Int, a:Int) -> Result<Int> in
if n<1 {
return .Done(a)
}
return .Call({
()->Result<Int> in
return fin(n: n-1,a: a+n)
})
}
let f = withTrampoline(fin)
f(30,0)
This code may be a bit longer than you think.
Because we need to use the current function inside the closure, we have to define a dummy implementation of the closure type before defining the real closure, so that the reference to itself is legalized in the closure implementation.
Without a puppet implementation, instead of declaring thefinclosure directly, you get the error that a variable was used during its initialization . If you are adventurous, try to replace this ugly solution with a Z-combination.
But if we remove the traditional trampoline design, we can simplify theResultenumeration and track the execution of the function within the trampoline, rather than the function as a value in the enumeration:
enum Result2<V,A>{
case Done(A)
case Call(V, A)
}
func withTrampoline2<V,A>(f:(V,A)->Result2<V,A>) -> ((V,A)->A){
return { (value:V,accumulator:A)->A in
var res = f(value,accumulator)
while true {
switch res {
case let .Done(accu):
return accu
case let .Call(num, accu):
res = f(num,accu)
}
}
}
}
let f2 = withTrampoline2 { (n:Int, a:Int) -> Result2<Int, Int> in
if n<1 {
return .Done(a)
}
return .Call(n-1,a+n)
}
f2(30,0)
This looks clearer and more compact.
You can get to Github or here for the playground file that this article uses
Alternatives to recursion in Swift
If you have read some articles on Swift functional programming, then you should know that Swift provides some useful features to replace recursion to solve some problems that would normally be solved using recursion.
For example, the number of triangles can be calculated by a simple line of functional code, using reduce:
(1...30).reduce(0,combine:+) //465
Or we can create a Sequence or Generator to generate a sequence of all the possible triangles:
class TriangularSequence :SequenceType {
func generate() -> AnyGenerator<Int> {
var i = 0
var acc = 0
return AnyGenerator(body:{
print("# Returning "+String(i))
i=i+1
acc = acc + i
return acc
})
}
}
var fs = TriangularSequence().generate()
for i in 1...30 {
print(fs.next())
}
These are the two possible alternatives that we can implement with Swift.
Conclusion
This article describes some of the limitations of recursive processing in swift and how to implement a trampoline in swift (a conventional optimization mechanism in a language lacking tail recursion optimization). But do I advocate the use of trampoline in code?
Cliff No.
In Swift, given that it is not a purely functional language, it is generally possible to solve problems with complex trampoline, and we can always solve them in a better way by some linguistic features (the code is more readable and the behavior is more understandable). Do not overdo the design of the code, the future you will thank yourself.
Tail recursion and trampoline translation in Swift programming