Tail Recursion. Problems with Recursion Recursion is generally favored over iteration in Scheme and many other languages – It’s elegant, minimal, can.

Slides:



Advertisements
Similar presentations
Introduction to Recursion and Recursive Algorithms
Advertisements

1 Scheme and Functional Programming Aaron Bloomfield CS 415 Fall 2005.
COMPILERS Register Allocation hussein suleman uct csc305w 2004.
Lists in Lisp and Scheme a. Lists are Lisp’s fundamental data structures, but there are others – Arrays, characters, strings, etc. – Common Lisp has moved.
Types of Recursive Methods
The University of Adelaide, School of Computer Science
Functional Programming. Pure Functional Programming Computation is largely performed by applying functions to values. The value of an expression depends.
Chapter 3 Functional Programming. Outline Introduction to functional programming Scheme: an untyped functional programming language.
Tail Recursion. Problems with Recursion Recursion is generally favored over iteration in Scheme and many other languages It’s elegant, minimal, can be.
Tail Recursion. Problems with Recursion Recursion is generally favored over iteration in Scheme and many other languages – It’s elegant, minimal, can.
CS 152: Programming Language Paradigms February 24 Class Meeting Department of Computer Science San Jose State University Spring 2014 Instructor: Ron Mak.
Stacks & Recursion. Stack pushpop LIFO list - only top element is visible top.
Functional Programming in Scheme and Lisp. Overview In a functional programming language, functions are first class objects. You can create them, put.
Stephen P. Carl - CS 2421 Recursion Reading : Chapter 4.
1 CS 177 Week 16 Recitation Recursion. 2 Objective To understand and be able to program recursively by breaking down a problem into sub problems and joining.
Computer Science and Software Engineering University of Wisconsin - Platteville 9. Recursion Yan Shi CS/SE 2630 Lecture Notes Partially adopted from C++
224 3/30/98 CSE 143 Recursion [Sections 6.1, ]
Edited by Malak Abdullah Jordan University of Science and Technology Data Structures Using C++ 2E Chapter 6 Recursion.
Data Structures R e c u r s i o n. Recursive Thinking Recursion is a problem-solving approach that can be used to generate simple solutions to certain.
Java Programming: Guided Learning with Early Objects Chapter 11 Recursion.
CS535 Programming Languages Chapter - 10 Functional Programming With Lists.
1 CS/COE0447 Computer Organization & Assembly Language Chapter 2 Part 3.
3/8/20161 Programming Languages and Compilers (CS 421) Reza Zamani Based in part on slides by Mattox Beckman, as updated.
Chapter 15: Recursion. Recursive Definitions Recursion: solving a problem by reducing it to smaller versions of itself – Provides a powerful way to solve.
Preocedures A closer look at procedures. Outline Procedures Procedure call mechanism Passing parameters Local variable storage C-Style procedures Recursion.
CSC 143 P 1 CSC 143 Recursion [Chapter 5]. CSC 143 P 2 Recursion  A recursive definition is one which is defined in terms of itself  Example:  Compound.
Recursion Chapter 2 Objectives Upon completion you will be able to:
CS314 – Section 5 Recitation 9
Lets and Loops Tail Recursion.
Functional Programming
Tail Recursion.
Chapter 10 Recursion Instructor: Yuksel / Demirer.
CSE341: Programming Languages Lecture 14 Thunks, Laziness, Streams, Memoization Dan Grossman Spring 2017.
CS 326 Programming Languages, Concepts and Implementation
CSE341: Programming Languages Lecture 6 Nested Patterns Exceptions Tail Recursion Dan Grossman Spring 2017.
CSE 341 Lecture 5 efficiency issues; tail recursion; print
Java 4/4/2017 Recursion.
CS 326 Programming Languages, Concepts and Implementation
Lists in Lisp and Scheme
Recursion
Data Structures Recursion CIS265/506: Chapter 06 - Recursion.
CSE341: Programming Languages Lecture 14 Thunks, Laziness, Streams, Memoization Zach Tatlock Winter 2018.
6.001 SICP Stacks and recursion
Recursion "To understand recursion, one must first understand recursion." -Stephen Hawking.
Important Concepts from Clojure
CSE341: Programming Languages Lecture 14 Thunks, Laziness, Streams, Memoization Dan Grossman Spring 2013.
Important Concepts from Clojure
Recursion Chapter 11.
Stacks & Recursion.
CSE341: Programming Languages Lecture 14 Thunks, Laziness, Streams, Memoization Dan Grossman Spring 2016.
CSE341: Programming Languages Lecture 6 Nested Patterns Exceptions Tail Recursion Dan Grossman Spring 2013.
The Metacircular Evaluator
CSE341: Programming Languages Lecture 14 Thunks, Laziness, Streams, Memoization Dan Grossman Autumn 2017.
CSE341: Programming Languages Lecture 6 Nested Patterns Exceptions Tail Recursion Dan Grossman Autumn 2018.
CSE341: Programming Languages Lecture 14 Thunks, Laziness, Streams, Memoization Dan Grossman Autumn 2018.
Streams, Delayed Evaluation and a Normal Order Interpreter
Lisp and Scheme I.
Streams and Lazy Evaluation in Lisp and Scheme
CSE341: Programming Languages Lecture 6 Nested Patterns Exceptions Tail Recursion Zach Tatlock Winter 2018.
Recursion Taken from notes by Dr. Neil Moore
Announcements Quiz 5 HW6 due October 23
Important Concepts from Clojure
Yan Shi CS/SE 2630 Lecture Notes
CSE341: Programming Languages Lecture 6 Nested Patterns Exceptions Tail Recursion Dan Grossman Spring 2016.
CSE341: Programming Languages Lecture 6 Nested Patterns Exceptions Tail Recursion Dan Grossman Spring 2019.
More Scheme CS 331.
Brett Wortzman Summer 2019 Slides originally created by Dan Grossman
Brett Wortzman Summer 2019 Slides originally created by Dan Grossman
CSE341: Programming Languages Lecture 14 Thunks, Laziness, Streams, Memoization Dan Grossman Spring 2019.
CSE341: Programming Languages Lecture 6 Nested Patterns Exceptions Tail Recursion Dan Grossman Autumn 2017.
Presentation transcript:

Tail Recursion

Problems with Recursion Recursion is generally favored over iteration in Scheme and many other languages – It’s elegant, minimal, can be implemented with regular functions and easier to analyze formally – Some languages don’t have iteration (Prolog) It can also be less efficient more functional calls and stack operations (context saving and restoration) Running out of stack space leads to failure deep recursion

Tail recursion is iteration Tail recursion is a pattern of use that can be compiled or interpreted as iteration, avoiding the inefficiencies Tail recursion A tail recursive function is one where every recursive call is the last thing done by the function before returning and thus produces the function’s value More generally, we identify some procedure calls as tail callstail calls

Tail Call A tail call is a procedure call inside another procedure that returns a value which is then immediately returned by the calling procedure def foo(data): bar1(data) return bar2(data) def foo(data): if test(data): return bar2(data) else: return bar3(data) A tail call need not come at the textual end of the procedure, but at one of its logical end points

Tail call optimization When a function is called, we must remember the place it was called from so we can return to it with the result when the call is complete This is typically stored on the call stackcall stack There is no need to do this for tail calls Instead, we leave the stack alone, so a newly called function will return its result directly to the original caller

Scheme’s top level loop Consider a simplified version of the REPL (define (repl) (printf “> “) (print (eval (read))) (repl)) This is an easy case: with no parameters there is not much context

Scheme’s top level loop 2 Consider a fancier REPL (define (repl) (repl1 0)) (define (repl1 n) (printf “~s> “ n) (print (eval (read))) (repl1 (add1 n))) This is only slightly harder: just modify the local variable n and start at the top

Scheme’s top level loop 3 There might be more than one tail recursive call (define (repl1 n) (printf “~s> “ n) (print (eval (read))) (if (= n 9) (repl1 0) (repl1 (add1 n)))) What’s important is that there’s nothing more to do in the function after the recursive calls

Two skills Distinguishing a trail recursive call from a non tail recursive one Being able to rewrite a function to eliminate its non-tail recursive calls

Simple Recursive Factorial (define (fact1 n) ;; naive recursive factorial (if (< n 1) 1 (* n (fact1 (sub1 n)) ))) Is this a tail call? No. It must be called and its value returned before the multiplication can be done

Tail recursive factorial (define (fact2 n) ; rewrite to just call the tail-recursive ; factorial with the appropriate initial values (fact2.1 n 1)) (define (fact2.1 n accumulator) ; tail recursive factorial calls itself ; as last thing to be done (if (< n 1) accumulator (fact2.1 (sub1 n) (* accumulator n)) )) Is this a tail call? Yes. Fact2.1’s args are evalua- ted before it’s called.

Trace shows what’s going on > (require racket/trace) > (load "fact.ss") > (trace fact1) > (fact1 6) |(fact1 6) | (fact1 5) | |(fact1 4) | | (fact1 3) | | |(fact1 2) | | | (fact1 1) | | | |(fact1 0) | | | |1 | | | 1 | | |2 | | 6 | |24 | 120 |

fact2 > (trace fact2 fact2.1) > (fact2 6) |(fact2 6) |(fact ) |(fact ) |(fact ) |(fact ) |(fact ) |(fact ) |(fact ) | Interpreter & compiler note the last expression to be evaluated & returned in fact2.1 is tail recursive call Instead of pushing state on the sack, it reassigns the local variables and jumps to beginning of the procedure Thus, the recursion is automatically transformed into iteration

Reverse a list This version works, but has two problems (define (rev1 lst) ; returns the reverse a list (if (null? lst) empty (append (rev1 (rest lst)) (list (first lst)))))) It is not tail recursive It creates needless temporary lists

A better reverse (define(rev2 list) (rev2.1 list empty)) (define (rev2.1 list reversed) (if (null? list) reversed (rev2.1 (rest list) (cons (first list) reversed))))

rev1 and rev2 > (load "reverse.ss") > (trace rev1 rev2 rev2.1) > (rev1 '(a b c)) |(rev1 (a b c)) | (rev1 (b c)) | |(rev1 (c)) | | (rev1 ()) | | () | |(c) | (c b) |(c b a) (c b a) > (rev2 '(a b c)) |(rev2 (a b c)) |(rev2.1 (a b c) ()) |(rev2.1 (b c) (a)) |(rev2.1 (c) (b a)) |(rev2.1 () (c b a)) |(c b a) (c b a) >

The other problem Append copies the top level list structure of it’s first argument. (append ‘(1 2 3) ‘(4 5 6)) creates a copy of the list (1 2 3) and changes the last cdr pointer to point to the list (4 5 6) In reverse, each time we add a new element to the end of the list, we are (re-)copying the list.

Append (two args only) (define (append list1 list2) (if (null? list1) list2 (cons (first list1) (append (rest list1) list2))))

Why does this matter? The repeated rebuilding of the reversed list is needless work It uses up memory and adds to the cost of garbage collection (GC) garbage collection GC adds a significant overhead to the cost of any system that uses it Experienced programmers avoid algorithms that needlessly consume memory that must be garbage collected

Fibonacci Another classic recursive function is computing the nth number in the fibonacci seriesfibonacci series (define (fib n) (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))) But its grossly inefficient – Run time for fib(n) ≅ O(2 n ) – (fib 100) can not be computed this way Are these tail calls?

This has two problems That recursive calls are not tail recursive is the least of its problems It also needlessly recomputes many values fib(6) Fib(5) Fib(4) Fib(3) Fib(2) Fib(3) Fib(2) Fib(1)

Trace of (fib 6) > (fib 6) > (fib 5) > >(fib 4) > > (fib 3) > > >(fib 2) > > > (fib 1) < < < 1 > > > (fib 0) < < < 0 < < <1 > > >(fib 1) < < <1 < < 2 > > (fib 2) > > >(fib 1) < < <1 > > >(fib 0) < < <0 < < 1 < <3 > >(fib 3) > > (fib 2) > > >(fib 1) < < <1 > > >(fib 0) < < <0 < < 1 > > (fib 1) < < 1 < <2 < 5 > (fib 4) > >(fib 3) > > (fib 2) > > >(fib 1) < < <1 > > >(fib 0) < < <0 < < 1 > > (fib 1) < < 1 < <2 > >(fib 2) > > (fib 1) < < 1 > > (fib 0) < < 0 < <1 < 3 <8 8 >

Tail-recursive version of Fib Here’s a tail-recursive version that runs in 0(n) (define (fib2 n) (cond ((= n 0) 0) ((= n 1) 1) (else (fib-tr n 2 0 1)))) (define (fib-tr target n f2 f1 ) (if (= n target) (+ f2 f1) (fib-tr target (+ n 1) f1 (+ f1 f2)))) fib-tr gets four args: target is the index of the number we want, n is the current index, f2 and f1 are the two previous fib numbers

Trace of (fib2 10) > (fib2 10) >(fib-tr ) >(fib-tr ) >(fib-tr ) >(fib-tr ) >(fib-tr ) >(fib-tr ) >(fib-tr ) >(fib-tr ) >(fib-tr ) < is the target, 5 is the current index fib(3)=2 and fib(4)=3 Stop when current index equals target and return sum of last two args

Compare to an iterative version The tail recursive version passes the “loop variables” as arguments to the recursive calls It’s just a way to do iteration using recursive functions without the need for special iteration operators def fib(n): if n < 3: return 1 else: f2 = f1 = 1 x = 3 while x<n: f1, f2 = f1 + f2, f1 return f1 + f2 def fib(n): if n < 3: return 1 else: f2 = f1 = 1 x = 3 while x<n: f1, f2 = f1 + f2, f1 return f1 + f2

General tail calls A call to any function that is in a tail position Can also be made without growing the call stack, though more work may need to be done if it is not a recursive call Some interpreters and compilers eliminate general tail calls as well – JVM only optimizes tail recursive calls – Scheme optimizes all tail calls

General tail calls Maximum call depth in a typical program w/o recursive functions isn’t large (e.g., order 10) Maximum call depth in programs with recursive functions can be huge (e.g., unbounded) Optimizing all tail calls is a way to handle indirect recursion (define (foo x n) (bar (+ x 1) n)) (define (bar x n) (if (> x n) x (foo x n))) Because Scheme privileges recursion over iteration it optimizes all tail calls

No tail call elimination in many PLs Many languages don’t optimize tail calls, including C and Python Recursion depth is constrained by the space allocated for the call stack This is a design decision that might be justified by the worse is better principleworse is better See Guido van Rossum’s comments on TRETRE

Python example > def dive(n=1):... print n,... dive(n+1)... >>> dive() Traceback (most recent call last): File " ", line 1, in File " ", line 3, in dive more lines... File " ", line 3, in dive RuntimeError: maximum recursion depth exceeded >>>

Conclusion Recursion is an elegant and powerful control mechanism We don’t need to use iteration We can eliminate any inefficiency if we Recognize and optimize tail-recursive calls, turning recursion into iteration Some languages (e.g., Python) choose not to do this, and advocate using iteration when appropriate But side-effect free programming remains easier to analyze and parallelize