Lab: Recursion with helper procedures

Assigned
Friday, Oct 12, 2018
Due
Lab writeups are due at 10:30pm on the day of the next class meeting. For example, a Wednesday lab is due at 10:30pm on Friday. Each lab writeup will be announced at the end of class on a lab day.
Summary
In this laboratory, you will explore recursive procedures in which we pass along intermediate computations, most typically using a recursive helper procedure. When this technique is used in conjunction with a program structure in which the recursive result is returned directly (without accumulated actions to perform), this technique supports tail recursion.

Preparation

a. Discuss the self check with your partner.

b. Do the normal lab setup. That is

  • Start DrRacket.
  • Make sure that the csc151 package is up to date.
  • Add (require csc151) to the top of the definitions pane.

c. Make a copy of the annotated new-sum-helper procedure from the reading, as well as new-sum and any accompanying documentation.

d. Verify that it does, in fact, print the expected information about intermediate procedure calls.

Exercises

Exercise 1: Logging procedure calls

a. Add the following code (taken from the reading) to your definitions pane.

;;; Procedure:
;;;   furthest-from-zero
;;; Parameters:
;;;   numbers, a nonempty list of real numbers
;;; Purpose:
;;;   Find the number in the list with the largest absolute value.
;;; Produces:
;;;   furthest, a real number
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   furthest is an element of numbers
;;;   For any number, n, in numbers
;;;     (>= (abs furthest) (abs n))
(define furthest-from-zero
  (lambda (numbers)
    (furthest-from-zero-helper (car numbers) (cdr numbers))))
(define furthest-from-zero-helper
  (lambda (furthest-so-far numbers-remaining)
    (if (null? numbers-remaining)
        furthest-so-far
        (furthest-from-zero-helper
         (further-from-zero furthest-so-far (car numbers-remaining))
         (cdr numbers-remaining)))))

;;; Procedure:
;;;   further-from-zero
;;; Parameters:
;;;   val1, a real number
;;;   val2, a real number
;;; Purpose:
;;;   Choose whichever of val1 and val2 is further from zero.
;;; Produces:
;;;   further, a real number
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   further is either val1 or val2
;;;   (abs further) >= (abs val1)
;;;   (abs further) >= (abs val2)
(define further-from-zero
  (lambda (val1 val2)
    (if (>= (abs val1) (abs val2))
        val1
        val2)))

b. Add appropriate calls to display and newline so that you can track all the calls to furthest-from-zero-helper and further-from-zero.

c. Sketch (that is, compute by hand) the output you expect to see in call to (furthest-from-zero (list -3 5 6 -2 1 -5)).

d. Check your answer by asking DrRacket to evaluate that expression.

Exercise 2: Product, revisited

a. Rewrite the product procedure, which computes the product of a list of values, using the same technique used for new-sum.

b. Write a similar my-quotient procedure. (Do not call it quotient, which is a built-in procedure that is commonly used.)

For example,

> (my-quotient (list 3))
3
> (my-quotient (list 3 5))
3/5
> (my-quotient (list 3 5 7))
3/35
> (my-quotient (list 3 5 7 6))
1/70
> (my-quotient (list 3 5 7 6 1/10))
1/7

Exercise 3: Tallying numbers

You may recall that in the previous lab, you wrote a procedure, tally-numbers, that took as input a list of mixed values (both numbers and non-numbers) and returned a count of the number of numbers in the list.

We will be rewriting tally-numbers using helper recursion. The helper procedure will likely have two parameters: a count of all the numbers you’ve seen so far and a list of all the values you have left to look at.

a. Make a table with two column labels: count-so-far and remaining. Here’s what we might expect those columns to have after we’ve done the first few steps of the helper on the list '(5 a b 1 c 2 3 b 4)

        tally-so-far                     remaining
        ------------                    ---------
        0                               '(5 a b 1 c 2 3 b 4)
        1                               '(a b 1 c 2 3 b 4)
        1                               '(b 1 c 2 3 b 4)
        1                               '(1 c 2 3 b 4)
        2                               '(c 2 3 b 4)

Fill in the next rows to suggest what might/should happen.

b. Using the ideas you gained in those steps, implement tally-numbers and tally-numbers-helper.

c. Check your answers on some of the following lists

> (tally-numbers null)
> (tally-numbers (list 1 2 3))
> (tally-numbers (list "a" "b" "c"))
> (tally-numbers (list 1 "a" 2 "b" 3 "c"))

d. Update tally-numbers-helper to print out calls, and call it on the list '(5 a b 1 c 2 3 b 4) to see if it matches your answer to part a.

Exercise 4: Selecting numbers

You may recall that in the reading on recursion basics, we wrote a procedure, select-numbers, that, given a list of values, returns a list with just the numbers from the original list.

Here’s an attempt to implement that procedure using helper recursion.

;;; Procedure:
;;;   select-numbers
;;; Parameters:
;;;   values, a list of Scheme values
;;; Purpose:
;;;   Create a new list values which consists of only the numbers
;;;   in values.
;;; Produces:
;;;   nums, a list of Scheme values
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   * number? holds for every value in nums.
;;;   * Every element of values for which number? holds appears in nums.
;;;   * Every element of nums appears in values.
;;;   * If a number appears multiple times in either list, it appears
;;;     the same number of times in both lists.
;;;   * The values retain their order.
(define select-numbers
  (lambda (values)
    (select-numbers-helper null values)))

(define select-numbers-helper
  (lambda (nums-so-far remaining)
    ; (display (list 'select-numbers-helper nums-so-far remaining)) (newline)
    (cond
     [(null? remaining)
      nums-so-far]
     [(not (number? (car remaining)))
      (select-numbers-helper nums-so-far (cdr remaining))]
     [else
      (select-numbers-helper (cons (car remaining) nums-so-far)
                             (cdr remaining))])))

a. Add the code and documentation to your definitions pane.

b. As you may have observed in the previous problem, it can be helpful to understand tail recursive procedures by creating a table whose columns are labeled with the parameters and which each row gives the values of those parameters in one call.

Make a table with two column labels: num-so-far and remaining. Here’s what we might expect those columns to have after we’ve done the first few steps of the helper on the list '(a b 1 c 2 3 b 4)

        num-so-far                      remaining
        ----------                      ---------
        '()                             '(a b 1 c 2 3 b 4)
        '()                             '(b 1 c 2 3 b 4)
        '()                             '(1 c 2 3 b 4)
        '(1)                            '(c 2 3 b 4)

Fill in the remaining five or so rows.

c. What output do you expect to get from (select-numbers '(a b 1 c 2 3 b 4))?

d. Check your answer experimentally.

e. If you did not get the answer you expected, uncomment the line that logs procedure calls and run it again.

Exercise 5: Selecting numbers, revisited

As you may have noted, the prior implementation of select-numbers produces the list of values in reverse order that they appear in the original list. Rewrite the procedure so that it produces the list of values in the same order as they appear in the original list. You may not use append in that implementation.

Exercise 6: Exploring costs

You may recall that we claimed that one of the implementations of furthest-from-zero using direct recursion was incredibly expensive.
Let’s check that claim.

a. Add the following definition of furthest-from-zero to your definitions pane.

(define furthest-from-zero-alt
  (lambda (numbers)
    (display (list 'furthest-from-zero-alt numbers)) (newline)
    (if (null? (cdr numbers))
        (car numbers)
        (if (>= (abs (car numbers)) 
                (abs (furthest-from-zero-alt (cdr numbers))))
            (car numbers)
            (furthest-from-zero-alt (cdr numbers))))))

b. What output do you expect from the following procedure call? (Detail both the answer and the log messages.)

> (furthest-from-zero-alt (list 8 5 3 1 0))

c. Check your answer experimentally.

d. What output do you expect from the following procedure call? (Detail both the answer and the log messages.)

> (furthest-from-zero-alt (list 0 1 3 5 8))

e. Check your answer experimentally.

f. Explain, in your own words, why one of the two results looks so much worse.

g. How many calls to furthest-from-zero-alt do you expect to see if we ask for the values of (furthest-from-zero-alt (iota 8))?

h. Check your answer experimentally.

Exercise 7: An alternate improvement

As we noted in the reading the problem with furthest-from-zero-alt is that we may have two recursive calls each time, rather than one. That’s dangerous. We suggested a few alternative strategies in the reading. Let’s consider another one. Suppose we use let to bind names to the car and the result of of the recursive call.

(let ([candidate (furthest-from-zero-alt (cdr numbers))]
      [alternate (car numbers)])

a. Rewrite furthest-from-zero-alt to use these names in place of the values they name.

b. What effect do you expect this naming to have on the efficiency of the procedure?

c. Check your answer experimentally.

For those with extra time

The following exercises will challenge you to extend the problem-solving strategies you’ve learned so far.

Extra 1: Tallying multiple types

Write a procedure, tally-numbers-strings-and-symbols, that takes as input a mixed list and produces a list of tallys of the number of numbers, strings, and values.

> (tally-numbers-strings-and-symbols null)
'(0 0 0)
> (tally-numbers-strings-and-symbols '(a 1 2 b "c" 3))
'(3 2 1)
> (tally-numbers-strings-and-symbols '(a b "c" "3" "e"))
'(0 2 3)

You will find it useful to base tally-numbers-strings-and-symbols on the helper-recursive tally-numbers your wrote earlier in this lab.

Extra 2: Checking preconditions

Rewrite the original furthest-from-zero so that the primary procedure (that is, furthest-from-zero) checks all of its preconditions before calling the helper.

Extra 3: Too many ways to find far values

The reading provides at least four ways to find the number furthest from zero in a list of numbers. You’ve developed at least one more in this lab. Which do you prefer, and why?

Let’s consider the first version of furthest-from-zero, which turned out to be incredibly inefficient.

(define furthest-from-zero-alt
  (lambda (numbers)
    (display (list 'furthest-from-zero-alt numbers)) (newline)
    (if (null? (cdr numbers))
        (car numbers)
        (if (>= (abs (car numbers)) 
                (abs (furthest-from-zero-alt (cdr numbers))))
            (car numbers)
            (furthest-from-zero-alt (cdr numbers))))))