;; 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-before) (read-case-sensitive #t) (teachpacks ()) (htdp-settings #(#t constructor repeating-decimal #f #t none #f () #f))) ;; Reading: Scott ยง6.6.1 (tail-recursion). ;; A video covering the first example of tail-recursion below (string-append-whoa) ;; is at: https://youtu.be/z-fWu762JKA ; see also: "the musical" https://www.youtube.com/watch?v=-PX0BV9hGZY&ab_channel=Confreaks ; Let's write the following function `string-append-whoa`: ; ;(check-expect (string-append-whoa '()) "whoa") ;(check-expect (string-append-whoa (list "aloha")) "aloha whoa") ;(check-expect (string-append-whoa (list "bye" "aloha")) "bye aloha whoa") ; N.B. we comment out check-expects only so that when clicking on 'stepper' button, ; we start w/ an interesting test. ; (Recommend: for stepper, actually pull out *just* this one test & def'n to separate file ; to avoid extraneous definitions being shown at each step.) (check-expect (string-append-whoa (list "hi" "bye" "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)))])) ; HOWEVER, we are now realizing that it builds up many stack frames ; (as we demo in the stepper). ; Compare to what we'd do in Java: #| 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 `strs` appended IN REVERSE ORDER, ; and ending in "whoa". ; (define (string-append-whoa2 strs) "stub") ; saw2-help : list-of-string, string -> string ; Return a string of all the elements in `strs` appended IN REVERSE ORDER, ; ... TODO: but what about `results-so-far`. ; ; (A space is included after each element of `strs`.) ; (define (saw2-help strs result-so-far) (cond [(empty? strs) "stub"] [(cons? strs) "stub" ;(first strs) (saw2-help (rest strs)) ])) (check-expect (saw2-help (list "already" "words" "all") "accumulated") "all words already accumulated") (check-expect (saw2-help (list "accumulated" "already" "words" "all") "") "all words already accumulated ") (check-expect (saw2-help '() "all words already accumulated") "all words already accumulated") (check-expect (string-append-whoa2 '()) "whoa") (check-expect (string-append-whoa2 (list "aloha")) "aloha whoa") (check-expect (string-append-whoa2 (list "bye" "aloha")) "aloha bye whoa") (check-expect (string-append-whoa2 (list "hi" "bye" "aloha")) "aloha bye hi whoa") ; 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)? ; Aside: ; Btw, we can now see why the two string-append programs above had reverse-order ; results from each other: ; if we don't build up stack-calls, we have to be processing left-to-right. ; ; (In this problem, we could "fix" it string-appending the item to the front ; instead of the back. In other situations, operations like `+` are commutative ; so one gets the same answer either way. But occasionally, if you have a singly- ; linked list, you can't process it w/o the overhead of a stack.) ;;;;;;;;;;;;;;;; Another 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)))])) #| 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; } |# ; 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) -999) ; 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) -999] [(cons? nums) -999 ;(first nums) (sum-help (rest nums)) ])) (check-expect (sum-help 0 '()) 0) (check-expect (sum-help 23 '()) 23) (check-expect (sum-help 23 (list 2)) 25) (check-expect (sum-help 23 (list 2 5)) 30) (check-expect (sum-help 23 (list 2 5 -3 1)) 28) (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) ;;;;;;;;;;;;;;;;;;;;;;;;;;; Another example: ;;;;;;;;;;;;;;;;;;;;;;;;;; ; `draw-enemies` from our video-game ;;; draw-enemies ; draw-enemies : list-of-enemies, image -> image ; Return an image like `bckgrnd` but with every element of `enemies` drawn onto it. ; (define (draw-enemies_v1 enemies bckgrnd) (cond [(empty? enemies) bckgrnd] [(cons? enemies) (draw-enemy (first enemies) (draw-enemies_v1 (rest enemies) bckgrnd))])) (define (draw-enemy dummy1 dummy2) "a place-holder") ;;;;;;;;;;;;;; another example of (converting to) tail-recursion ;;;;;;;; ;;; my-max #| java loop for finding max of a list if (nums.isEmpty()) throw new ArgumentException("can't take max of empty list"); maxSoFar = nums.get(0); nums = nums.sublist(1); while (!nums.isEmpty()) { maxSoFar = (nums.get(0)>maxSoFar) ? nums.get(0) : maxSoFar; nums = nums.sublist(1); } |# ; FIXED VERSION of `my-max`, to correctly (not) handle empty-lists ; - use `let` OR call `max-of-two` (or the built-in `max`) ; - this becomes a wrapper for `my-max-or` (fixes sentinel) ; my-max : cons -> number ; Return the largest element of `a-lon`, ; (define (my-max nums) (cond [(empty? nums) (error 'my-max "can't take max of empty list")] [(cons? nums) -999])) ; we might also make this function accept *any* list, ; but throw our own exception if we were passed the empty-list. ; This is like Java's philosophy of "UnsupportedOperationException". ; But `UnsupportedOperationException` feels wrong: ; If you say this function works on anything of type List ; you're just flat out lying! ; (even if your weasel-words are "and we define the correct ; behavior as *requiring* an exception on the empty list"). ; In Java, it's usually a violation of the Liskov Substitution Principle. ; my-max-or : list-of-num, num -> num ; Return the largest out of: the elements of `nums` (if any) AND the number `max-so-far`. ; (define (my-max-or nums max-so-far) (cond [(empty? nums) -999] [else -999 ; (first nums) (my-max-or (rest nums)) ])) (check-expect (my-max-or '() 7) 7) (check-expect (my-max-or (cons 5 '()) 4) 5) (check-expect (my-max-or (cons 5 '()) 7) 7) (check-expect (my-max-or (cons 3 (cons 5 '())) 7) 7) (check-expect (my-max-or (cons 3 (cons 5 '())) 4) 5) (check-expect (my-max-or (cons 5 (cons 3 '())) 4) 5) (check-expect (my-max-or (cons 5 (cons 3 '())) 7) 7) ;;;;;;;;;;;;;;;;;;;; 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) -999) (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) -999] [else -999])) (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` !