0% found this document useful (0 votes)
83 views16 pages

Linear: Has A Call-Tree

The document discusses tail recursion in SML. It provides examples of reversing a list, summing a list, and calculating Fibonacci numbers recursively in both a tail-recursive and non-tail-recursive way. Tail-recursive functions can be implemented iteratively by a compiler as loops without using additional stack space, improving performance compared to non-tail-recursive versions. Accumulator parameters are used to evaluate functions from left to right and make recursion tail-recursive.

Uploaded by

Ashish Dobriyal
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
83 views16 pages

Linear: Has A Call-Tree

The document discusses tail recursion in SML. It provides examples of reversing a list, summing a list, and calculating Fibonacci numbers recursively in both a tail-recursive and non-tail-recursive way. Tail-recursive functions can be implemented iteratively by a compiler as loops without using additional stack space, improving performance compared to non-tail-recursive versions. Accumulator parameters are used to evaluate functions from left to right and make recursion tail-recursive.

Uploaded by

Ashish Dobriyal
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 16

Call Trees

fun s u m l i s t n i l = 0 | s u m l i s t ( x : : xs ) = x + s u m l i s t xs

(Tail) Recursion Amtoft from Hatcli Run-Time Structures Accumulators Tail Recursion Further Examples Summary

has a linear call-tree


sum list ([2 ,1]) | sum list ([1]) | sum list ( nil ) fun f i b 0 = 0 | fib 1 = 1 | f i b n = f i b ( n1) + f i b ( n2)

has a non-linear (branching) call-tree


f i b (3) / \ f i b (2) f i b (1) / \ f i b (0) f i b (1)

Stacking Bindings
fun r e v e r s e n i l = n i l | r e v e r s e ( x : : xs ) = r e v e r s e xs @ [ x ] val L = [ 1 , 2 , 3 ] ; reverse (L ) ;

(Tail) Recursion Amtoft from Hatcli Run-Time Structures Accumulators Tail Recursion Further Examples Summary

Environment during recursion: (see p. 67)


++ | | ++ | xs nil | | x 3 | ++ | xs [3] | | x 2 | ++ | xs [2 ,3] | | x 1 | ++ | L [1 ,2 ,3] | | ... | ++ . . added i n r e v e r s e ( n i l )

. . added i n r e v e r s e ( [ 3 ] )

. . added i n r e v e r s e ( [ 2 , 3 ] )

. . added i n r e v e r s e ( [ 1 , 2 , 3 ] )

. . top l e v e l environment

Running Time

(Tail) Recursion Amtoft from Hatcli Run-Time Structures Accumulators Tail Recursion Further Examples

fun r e v e r s e n i l = n i l | r e v e r s e ( x : : xs ) = ( r e v e r s e xs ) @ [ x ]

Summary

Consider calling reverse on a list of length n


it makes n calls to append which takes time 1, 2, . . . n 2, n 1, n

the running time is thus quadratic.

Performance Test
We need generator of large data:
fun from i j = i f i > j then n i l e l s e i : : from ( i +1) j

(Tail) Recursion Amtoft from Hatcli Run-Time Structures Accumulators Tail Recursion Further Examples Summary

Execute reverse L where L is the value of (from 1 n) n 10,000 20,000 40,000 100,000 running time 2 seconds 7 seconds 34 seconds very slow

When testing sum list, we rather want


fun o n e s 0 = n i l | o n e s n = 1 : : o n e s ( n1)

Assessment

(Tail) Recursion Amtoft from Hatcli Run-Time Structures Accumulators Tail Recursion

fun r e v e r s e n i l = n i l | r e v e r s e ( x : : xs ) = ( r e v e r s e xs ) @ [ x ]

Further Examples Summary

Why must we call append? :: only allows us to add items in front of list reverse does non-trivial computation only when going up the tree We might consider doing computation when going down the tree

Passing Results Down In Call Tree


Recall that list reversal is special case of foldl
fun f o l d l f e n i l = e | f o l d l f e ( x : : xs ) = f o l d l f ( f (x , e )) xs fun m y r e v e r s e x s = f o l d l op : : n i l xs ;

(Tail) Recursion Amtoft from Hatcli Run-Time Structures Accumulators Tail Recursion Further Examples Summary

Specializing foldl wrt op:: yields


fun r e v a c c e n i l = e | rev acc e ( x : : xs ) = rev acc ( x : : e ) xs fun r e v e r s e a c c x s = r e v a c c n i l x s

e holds the results so far e is owing down the tree, informing the recursion at the next level of something that we have accumulated at the current level

Performance Comparison

(Tail) Recursion Amtoft from Hatcli Run-Time Structures Accumulators

Recall that reverse had quadratic running time. Since reverse acc uses no append, we expect linear running time. When called on the value of from 1 n n 10,000 20,000 100,000 1,000,000 reverse 2 seconds 7 seconds very slow infeasible reverse acc instantaneous instantaneous instantaneous 3 seconds

Tail Recursion Further Examples Summary

Tail Recursion

(Tail) Recursion Amtoft from Hatcli Run-Time Structures Accumulators Tail Recursion

fun r e v a c c e n i l = e | rev acc e ( x : : xs ) = rev acc ( x : : e ) xs

Further Examples Summary

This function is tail recursive: no computation happens after the recursive call value of recursive call is the return value thus, no variables are referenced after recursive call This kind of recursion is actually iteration in disguise!

Iterative Reverse
fun r e v a c c e n i l = e | rev acc e ( x : : xs ) = rev acc ( x : : e ) xs

(Tail) Recursion Amtoft from Hatcli Run-Time Structures Accumulators Tail Recursion

can be converted to pseudo-C (renaming e to acc):


l i s t r e v e r s e ( xs : l i s t ) { l i s t acc ; acc = [ ] ; w h i l e ( x s != n i l ) do { a c c = hd ( x s ) : : a c c ; xs = t l ( xs ) ; } return acc ; }

Further Examples Summary

acc holds result xs and acc are updated each time through the loop

Tail Recursion versus Non-Tail Recursion


( v e r s i o n 1 : without accumulator ) fun r e v e r s e n i l = n i l | r e v e r s e ( x : : xs ) = r e v e r s e xs @ [ x ] ( v e r s i o n 2 : with accumulator ) fun r e v a c c e n i l = e | rev acc e ( x : : xs ) = rev acc ( x : : e ) xs

(Tail) Recursion Amtoft from Hatcli Run-Time Structures Accumulators Tail Recursion Further Examples Summary

x is used after recursion in v.1, but not in v.2 for tail-recursive functions, we do thus not need to stack variable bindings for the recursive calls parameter passing can be implemented in the compiler by destructive updates (that is, assignment)! Computation occurs after recursion in v.1, but not in v.2 for tail-recursive functions, we do thus not need to stack return addresses; a call can be implemented in the compiler as a goto!

Parameter Assignment
The tail-recursive function
fun f ( y 1 , . . . , y n ) = ... f (<exp 1>, . . . , <expn>)

(Tail) Recursion Amtoft from Hatcli Run-Time Structures Accumulators Tail Recursion Further Examples Summary

...is roughly equivalent to...


... f(y 1 , ... , y n) { while . . . { ... ... y 1 = <exp 1>; ... y n = <expn >; } }

Converting SumList to Tail Recursion


fun s u m l i s t n i l = 0 | s u m l i s t ( x : : xs ) = x + s u m l i s t xs

(Tail) Recursion Amtoft from Hatcli Run-Time Structures Accumulators

The recursive calls are unfolded until we reach the end of the list, from where we then move to the left while summing the results.
fun s u m l i s t a c c a c c n i l = a c c | s u m l i s t a c c acc ( x : : xs ) = s u m l i s t a c c ( x+a c c ) x s

Tail Recursion Further Examples Summary

Summation proceeds while moving left to right. Top-level call: sum list acc 0 xs Performance comparison on the value of ones n n 4,000,000 5,000,000 sum list 5 seconds 21 seconds sum list acc instantaneous instantaneous

Tail-Recursive MultList
fun m u l t l i s t a c c a c c n i l = a c c | m u l t l i s t a c c acc ( x : : xs ) = m u l t l i s t a c c ( x acc ) xs

(Tail) Recursion Amtoft from Hatcli Run-Time Structures Accumulators Tail Recursion Further Examples Summary

Question: what happens if we hit a 0?


fun m u l t l i s t a c c e x i t a c c n i l = a c c | m u l t l i s t a c c e x i t acc ( x : : xs ) = i f x = 0 then 0 e l s e m u l t l i s t a c c e x i t ( x acc ) xs

In C, we might have
int m u l t l i s t ( xs : l i s t ) { i n t acc ; acc = 1 ; w h i l e ( x s != n i l ) do { i f ( hd ( x s ) = 0 ) t h e n return 0; / e s c a p e / else a c c = hd ( x s ) a c c ; xs = t l ( xs ) ; } r e t u r n acc ;

Making Fibonacci Tail-Recursive


fun f i b 0 = 0 | fib 1 = 1 | f i b n = f i b ( n2) + f i b ( n1)

(Tail) Recursion Amtoft from Hatcli Run-Time Structures Accumulators Tail Recursion Further Examples Summary

has a branching call-tree, and can be made tail-recursive by using two accumulating parameters:
fun f i b a c c p r e v c u r r n = i f n = 1 then c u r r e l s e f i b a c c c u r r ( p r e v+c u r r ) ( n1) fun f i b o n a c c i a c c n = i f n = 0 then 0 e l s e f i b a c c 0 1 n

Performance comparison n 42 43 44 fib 7 seconds 11 seconds 17 seconds fibonacci acc instantaneous instantaneous instantaneous

Correctness of Tail-Recursive Fibonacci


With F the bonacci function we have
F (0) = 0; F (1) = 1; F (n) = F (n 2) + F (n 1)

(Tail) Recursion Amtoft from Hatcli Run-Time Structures Accumulators Tail Recursion Further Examples

which can be tail-recursively implemented by


fun g ( n , p r e v , c u r r ) = i f n = 1 then c u r r e l s e g ( n 1, c u r r , p r e v+c u r r )

Summary

Correctness Lemma: for all n 1, k 0: g (n, F (k), F (k + 1)) = F (n + k) This can be proved by induction in n. the base case is n = 1 which is obvious. for the inductive case, n > 1,
g (n, F (k), F (k+1)) = g (n1, F (k+1), F (k)+F (k+1)) = g (n1, F (k+1), F (k+2)) = F ((n1)+(k+1)) = F (n+k)

Thus F (n) = g (n, F (0), F (1)) = g (n, 0, 1).

Summary

(Tail) Recursion Amtoft from Hatcli Run-Time Structures Accumulators

a tail-recursive function is one where the function performs no computation after the recursive call a good SML compiler will detect tail-recursive functions and implement them iteratively
as loops there is no need to stack bindings or return addresses recursive calls become gotos we can think of arguments as being assigned to (destructively update) formal parameters.

Tail Recursion Further Examples Summary

this substantially reduces execution time and space (for stack) overhead

You might also like