;; 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-intermediate-lambda-reader.ss" "lang")((modname tail-recursion) (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) (cond [(empty? strs) "whoa"] [(cons? strs) (string-append (first strs) " " (string-append-whoa (rest strs)))])) ;(string-append-whoa (list "hi" "bye" "aloha")) ; Here's an equivalent 'spelled out' version which uses the template: ; HOWEVER, we are now realizing that it builds up many stack frames ; (as demo'd in the stepper): #| // 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")) "aloha bye hi 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 IN REVERSE ORDER, ; and ending in `results-so-far`. ; (A space is included after each element of `strs`.) ; (define (saw2-help strs result-so-far) (cond [(empty? strs) result-so-far] [(cons? strs) (saw2-help (rest strs) (string-append (first strs) " " result-so-far))])) (check-expect (saw2-help (cons "already" (cons "words" (cons "all" '()))) "accumulated") "all words already accumulated") (check-expect (saw2-help (cons "accumulated" (cons "already" (cons "words" (cons "all" '())))) "") "all words already accumulated ") (check-expect (saw2-help '() "all words already accumulated") "all words already accumulated") ; A function is 'tail recursive' if its recursive call is the last thing it ; needs to do. ; (It doesn't need 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 optimizaiton. ; 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 0 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) (cond [(empty? nums) sum-so-far] [(cons? nums) (sum-help (+ (first nums) sum-so-far) (rest 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) ;;; A third example of converting to tail-recursion: ;;; the Collatz conjecture; See: ;;; https://en.wikipedia.org/wiki/Collatz_conjecture#Statement_of_the_problem ;;; ; once : return the collatz-function applied just once ; (that is, return n/2 or 3n+1, depending on n's parity). ; (define (once n) (if (even? n) (/ n 2) (+ (* 3 n) 1))) (check-expect (once 3) 10) (check-expect (once 10) 5) (check-expect (once 5) 16) (check-expect (once 16) 8) (check-expect (once 8) 4) (check-expect (once 4) 2) (check-expect (once 2) 1) (check-expect (once 1) 4) ; collatz-count : natnum -> natnum ; return the #steps until 1 is reached, for Collatz sequence starting at n. ; (define (collatz-count n) (cond [(= n 1) 0] [else (+ 1 (collatz-count (once n)))])) (check-expect (collatz-count 1) 0) (check-expect (collatz-count 2) 1) (check-expect (collatz-count 4) 2) (check-expect (collatz-count 5) 5) ;;; The above code does NOT follow the template for natnums ;;; (it doesn't recur on `(sub1 n)`), so we don't have a guarantee of termination. ;;; (Indeed, whether it always stops is exactly the collatz conjecture.) ;;;; converting tail-recur ; collatz-count-v2 : natnum -> natnum ; return the #steps until 1 is reached, for Collatz sequence starting at n. ; (define (collatz-count-v2 n) (helper n 0)) (check-expect (collatz-count-v2 1) 0) (check-expect (collatz-count-v2 2) 1) (check-expect (collatz-count-v2 4) 2) (check-expect (collatz-count-v2 5) 5) ; helper : natnum, natnum -> natnum ; return the #steps until 1 is reached, for Collatz sequence starting at n, PLUS steps-so-far ; (define (helper n steps-so-far) (cond [(= n 1) steps-so-far] [else (helper (once n) (+ 1 steps-so-far))])) (check-expect (helper 1 17) 17) (check-expect (helper 2 17) 18) (check-expect (helper 4 17) 19) (check-expect (helper 5 17) 22) ;;;;;;;;;; ;; 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)) ; Coming up -- writing `while/list` !