λ λ CS 251 Fall 2019 CS 251 Spring 2020 Topics Principles of Programming Languages Principles of Programming Languages Ben Wood Ben Wood Recursion is an elegant and natural match for many computations and data structures. Tail Recursion • Natural recursion with immutable data can be space- inefficient compared to loop iteration with mutable data. • Tail recursion eliminates the space inefficiency with a Ta +tail.rkt simple, general pattern. • Recursion over immutable data expresses iteration more clearly than loop iteration with mutable state. • More higher-order patterns: fold https://cs.wellesley.edu/~cs251/s20/ 1 Tail Recursion 2 Tail Recursion Naturally recursive factorial CS 240-style machine model Co Code St Stack Registers Re Call frame (define (fact n) fixed size, general purpose (if (= n 0) 1 Call frame (* n (fact (- n 1))))) Call frame Hea Heap arguments, variables, return address per function call Space: Sp e: O( ) Program How efficient is this implementation? Counter cons cells, data structures, … Ti Time: O( ) Stack Pointer Tail Recursion 3 Tail Recursion 4
Evaluation Naturally recursive factorial (define (fact n) example (if (= n 0) 1 (* n (fact (- n 1))))) Call stacks at each step (define (fact n) (fact 3) (fact 3): 3*_ (fact 3): 3*_ (fact 3): 3*_ Compute result so far (fact 2) (fact 2): 2*_ (fact 2): 2*_ (if (= n 0) af after/from recursive call. 1 Base case returns (fact 1) (fact 1): 1*_ base result. (* n (fact (- n 1))))) Remember : n ↦ 2 ; and (fact 0) Recursive case returns “rest of function” for this call. Compute remaining argument result so far. before/for recursive call. be (fact 3): 3*_ (fact 3): 3*_ (fact 3): 3*_ (fact 3): 3*2 (fact 2): 2*_ (fact 2): 2*_ (fact 2): 2*1 Space: Sp e: O( ) (fact 1): 1*_ (fact 1): 1*1 Ti Time: O( ) (fact 0): 1 Tail Recursion 5 Tail Recursion 6 Tail recursive factorial Common patterns of work Natural recursion: Tail recursion: Accumulator parameter provides result so far. (define (fact n) Argument Full result Argument Base result (define (fact-tail n acc) Compute result so far (if (= n 0) be before/for recursive call. acc Deeper recursive calls Base case returns Reduce Re Reduce Re full result. (fact-tail (- n 1) (* n acc)))) argument argument Recursive case returns Accumulate Ac Ac Accumulate Compute remaining argument full result. result result before/for recursive call. be (fact-tail n 1)) so far so far Initial accumulator provides base result. Base case Base result Base case Full result Tail Recursion 7 Tail Recursion 8
ar argume ment fu full ll result lt argume ar ment ba base result Natural Tail (fact 4): 24 (fact-tail 4 1): 24 recursion recursion -1 -1 * * accumulate accumulate reduce (fact 3): 6 reduce (fact-tail 3 4): 24 -1 -1 * * Recursive case: Recursive case: Compute result Compute recursive argument (fact 2): 2 (fact-tail 2 12): 24 in terms of argument and in terms of argument and accumulated recursive result. -1 accumulator. -1 * * (fact 1): 1 (fact-tail 1 24): 24 -1 -1 * * (fact 0): 1 (fact-tail 0 24): 24 (define (fact n) (define (fact n) ba base case ba base result base case ba full fu ll result lt (define (fact-tail n acc) (if (= n 0) (if (= n 0) 1 acc (* n (fact (- n 1))))) (fact-tail (- n 1) (* n acc)))) (fact-tail n 1)) Tail Recursion 9 Tail Recursion 10 Evaluation example Tail-call optimization Nothing useful remembered here. Call stacks at each step Space: Sp e: O( ) (define (fact n) (fact 3) (fact 3): _ (fact 3): _ (fact 3): _ (define (fact-tail n acc) (ft 3 1) (ft 3 1):_ (ft 3 1):_ (if (= n 0) Time: O( ) Ti acc (ft 2 3) (ft 2 3):_ ft = fact-tail (fact-tail (- n 1) (* n acc)))) (fact-tail n 1)) (ft 1 6) (fact 3) (ft 3 1) (ft 2 3) (ft 1 6) (ft 0 6) (fact 3): _ (fact 3): _ (fact 3): _ (fact 3): _ Language implementation recognizes tail calls. (ft 3 1):_ (ft 3 1):_ (ft 3 1):_ (ft 3 1):_ • Caller frame never needed again. (ft 2 3):_ (ft 2 3):_ (ft 2 3):_ (ft 2 3):6 • Reuse same space for every recursive tail call. • Low-level: acts just like a loop. (ft 1 6):_ (ft 1 6):_ (ft 6 1):6 etc. (ft 0 6) (ft 0 6):6 Racket, ML, most “functional” languages, but not Java, C, etc. Tail Recursion 11 Tail Recursion 12
Ta Tail call intuition: “nothing left for caller to do after call”, Tail recursion transformation Tail position “callee result is immediate caller result” Common pattern for transforming naturally recursive functions to tail-recursive form. Works for functions that do commutative operations (order of steps doesn't matter). (define (fact n) natur na ural rec ecur ursion Recursive definition of ta tail il p positio ition: (if (= n 0) – In (lambda (x1 … xn) e ) , the body e is in tail position. 1 – If If (if e1 e2 e3 ) is is in in tail il posit itio ion , (* n (fact (- n 1)) ) )) then e2 and e3 are in tail position (but e1 is not). – If If (let ([x1 e1] … [xn en]) e ) is is in in tail il posit itio ion , then e is in tail position (but the binding expressions are not). tail ta il r recursio ion (define (fact n) No Note: (define (fact-tail n acc ) • If a non-lambda expression is not in tail position, then no subexpressions are. (if (= n 0) • Critically, in a function call expression (e1 e2) , Ac Accumulator subexpressions e1 and e2 are no not in tail position. acc becomes be (fact-tail (- n 1) (* n acc ) ))) ba base result. A ta tail call is a function call in tail position. (fact-tail n 1 )) Re Recursive ve step applied to ac accumulat ator A function is ta tail-re recursive if it uses a recursive tail call. in instead d of re recursive re result. Bas Base resul ult becomes init in itia ial l accumula lator. Tail Recursion 13 Tail Recursion 14 (order matters) Practice: use the transformation Transforming non-commutative steps (define (reverse-natural-slow xs) ;; Naturally recursive sum (if (null? xs) (define (sum-natural xs) null (if (null? xs) (append (reverse-natural-slow (cdr xs)) 0 (list (car xs))))) ✘ (+ (car xs) (sum-natural (cdr xs))))) (define (reverse-tail-just-kidding xs) (define (rev xs acc) ;; Tail-recursive sum (if (null? xs) (define (sum-tail xs) acc (rev (cdr xs) (append acc (list (car xs)))))) (define (sum-onto xs acc) (rev xs null)) (if (null? xs) ✓ acc (define (reverse-tail-slow xs) (define (rev xs acc) (sum-onto (cdr xs) (+ (car xs) acc))) (if (null? xs) acc (sum-onto xs 0)) (rev (cdr xs) (append (list (car xs)) acc)))) (rev xs null)) Tail Recursion 15 Tail Recursion 16
Tail recursion ≠ accumulator pattern The transformation is not always ideal. O( ) (define (reverse-tail-slow xs) ; mutually tail recursive (define (rev xs acc) (define (even n) (if (null? xs) (or (zero? n) (odd (- n 1)))) acc (define (odd n) (rev (cdr xs) (append (list (car xs)) acc)))) (or (not (zero? n)) (even (- n 1)))) (rev xs null)) (define (reverse-tail-good xs) ; tail recursive O(n) (define (rev xs acc) (define (even2 n) (if (null? xs) (cond [(= 0 n) #t] acc [(= 1 n) #f] (rev (cdr xs) (cons (car xs) acc)))) [#t (even2 (- n 2))])) (rev xs null)) • Tail recursion and the accumulator pattern are co commonl nly y • Append-based recursive reverse is O(n 2 ): each recursive call must us used toge geth ther . They are no not sy synon onyms . traverse to end of list and build a fully new list. – 1+2+…+(n-1) is almost n*n/2 – Natural recursion may use an accumulator. – Moral: beware append, especially within recursion – Tail recursion does not necessarily involve an accumulator. • Tail-recursive reverse can avoid append in O(n). What about map, filter? – Cons is O(1), done n times. Tail Recursion 17 Tail Recursion 18 Why tail recursion instead of Identify dependences between ________. loops with mutation? Racket: immutable Ra le natural l recursion (define (fib n) recursiv re ive (if (< n 2) calls alls 1. Simpler language, but just as efficient. n (+ (fib (- n 1)) (fib (- n 2))))) 2. Explicit dependences for easier reasoning. Racket: immutable Ra le tail l recursion (define (fib n) – Especially with HOFs like fold! (define (fib-tail n fibi fibi+1) (if (= 0 n) fibi (fib-tail (- n 1) fibi+1 (+ fibi fibi+1)))) (fib n 0 1)) def fib(n): Python: lo loop iterat ation with mutat ation fib_i = 0 fib_i_plus_1 = 1 loop lo for i in range(n): fib_i_prev = fib_i iterat it ratio ions fib_i = fib_i_plus_1 fib_i_plus_1 = fib_i_prev + fib_i_plus_1 return fib_i Tail Recursion 19 Tail Recursion 20
Recommend
More recommend