Recursion with helper procedures
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-kernel
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))))))