;; The first three lines of this file were inserted by DrRacket. They record metadata ;; about the language level of this file in a form that our tools can easily process. #reader(lib "htdp-beginner-reader.ss" "lang")((modname tail-recursion-start) (read-case-sensitive #t) (teachpacks ()) (htdp-settings #(#t constructor repeating-decimal #f #t none #f () #f))) ;; FYI: Scott ยง6.6.1 also discusses tail-recursion. ; Let's write the following function `string-append-whoa`: ; (check-expect (string-append-whoa '()) "whoa") (check-expect (string-append-whoa (cons "aloha" '())) "aloha whoa") (check-expect (string-append-whoa (cons "bye" (cons "aloha" '()))) "bye aloha whoa") (check-expect (string-append-whoa (cons "hi" (cons "bye" (cons "aloha" '())))) "hi bye aloha whoa") ; string-append-whoa : list-of-strings -> string ; Return a string of all the elements in `whoa` appended, and ending in "whoa". ; (define (string-append-whoa strs) (if (equal? strs 'no-way) "ha ha" "blah")) ;(string-append-whoa (list "hi" "bye" "aloha")) ; If we step through this with the stepper, ; we see that this template-derived version builds up many pending stack frames. ; Compare to what we'd do in Java: #| // Java loop version of the above: // String stringAppendWhoa( List strs ) { String resultSoFar = "whoa"; while (!strs.isEmpty()) { resultSoFar = strs.get(0) + " " + resultSoFar; strs = strs.subList(1,strs.size()); } return resultSoFar; } // Tracing through -- at the start of each iteration: // strs resultSoFar // ---------- ----------- // {"hi","bye","aloha"} "whoa" // {"bye","aloha"} "hi whoa" // {"aloha"} "bye hi whoa" // {} "aloha bye hi whoa" // Why doesn't this loop build up stack frames? // Because the `while` turns into // a "goto start of loop, with revised `resultSoFar` and `strs`." |# #| ; If we also use a 'so-far' variable, then we can ; write a racket version which doesn't build up stack-frames: (check-expect (string-append-whoa2 (list "hi" "bye" "aloha")) "hi bye aloha whoa") ; string-append-whoa2 : list-of-string -> string ; Return a string of all the elements in `whoa` appended, and ending in "whoa". ; (define (string-append-whoa2 strs) (saw2-help strs "whoa")) ; saw2-help : list-of-string, string -> string ; Return a string of all the elements in `strs` appended, and ending in `results-so-far`. ; (define (saw2-help strs result-so-far) ...) ; A function is 'tail recursive' if its recursive call is the last thing it ; needs to do. ; (It never needs to combine the *results* of the recursive call in any way.) ; We say "the recursive call is in tail position". ; An optimization: if a recursive call is in tail position, ; then don't allocate a new stack frame; just re-use the current one! ; This can turn a function which would use O(n) stack-space into O(1) stack space. ; To make our regular recursive function into a *tail recursive* one, ; we used the trick: ; add an "accumulator" variable (or, a "so-far" variable). ; Scheme (racket): compiler is *required* to implement tail-recursion optimization. ; Java : cannot implement tail recursion (the jvm spec doesn't allow it, and ; Java must be backwards-compatible). The reason is ??: to preserve ; stack-traces in the presence of exceptions (which might get thrown ; anywhere)? ;;;;; One more example: summing number in a list: ; sum_v1 : (listof number) -> number ; Return the sum of all numbers in `nums`. ; This v1 follows the template -- is *not* tail-recursive. ; (define (sum-v1 nums) (cond [(empty? nums) 0] [(cons? nums) (+ (first nums) (sum-v1 (rest nums)))])) ; sum-v2 : (listof number) -> number ; ; Return the sum of all numbers in `nums`. ; This v2 follows adds an accumulator -- *is* tail-recursive. ; (define (sum-v2 nums) ...) ; sum-help : number, (listof number) -> number ; Return the sum of all the numbers in `nums`, added to `sum-so-far`. ; See also: the loop inside sum_v2 in Java. ; (define (sum-help sum-so-far nums) ...) #| The corresponding Java code for sum-v2 and the loop sum-help: double sum_v2( List nums ) { double sumSoFar = 0.0; while (!nums.isEmpty()) { // Loop invariant: sumSoFar + sum of numbers in `nums` is same, // each time through the loop. sumSoFar = nums.get(0) + sumSoFar; // Cf. "sumSofar = (first nums) + sumSoFar" nums = nums.subList(1,nums.size()); // Cf. "nums = (rest nums)" } // Reach here when nums.isEmpty: return sumSoFar; } |# (check-expect (sum-v1 '()) 0) (check-expect (sum-v1 (list 2)) 2) (check-expect (sum-v1 (list 2 5)) 7) (check-expect (sum-v1 (list 2 5 -3 1)) 5) (check-expect (sum-v2 '()) 0) (check-expect (sum-v2 (list 2)) 2) (check-expect (sum-v2 (list 2 5)) 7) (check-expect (sum-v2 (list 2 5 -3 1)) 5) ;;;;;;;;;; ;; To think about: ; Racket doesn't have a 'while' loop built in (at least, the student-languages don't). ; But we've just seen that tail-recursion is, in its essence, a loop. ; So can we write our own while-loop in racket, *as a [tail-recursive] function* ?!? ; How would we call it? ; Imagine a function like: ; (check-expect (nums-down-from 7) (list 7 6 5 4 3 2 1)) ; or like: (sqrts-down-from 17) = (list 4.101 4 3.98 ... 1.414 1) ; If we had a type of while-loop which collected the result of each iteration ; into a list, then we would be able to right `nums-down-from` very easily. ; Imagine: (define (nums-down-from n) (while/list n positive? sub1 identity)) (define (sqrts-down-from n) (while/list n positive? sub1 sqrt)) ; Coming up -- writing `while/list` ! (define (while/list i continue? end-of-loop-update body) 'hmmm) |#