Laboratory Exercises For Computer Science 151

With the help of recursion, we can perform another common operation on lists: filtering. One might, for example, write a procedure that takes any list of real numbers and returns a similar list, but with all the negative numbers removed; such a procedure would ``filter out'' the negative numbers. Here's how it would look:

(define filter-out-negatives
  (lambda (ls)
    (cond ((null? ls) '())
          ((negative? (car ls)) (filter-out-negatives (cdr ls)))
          (else (cons (car ls) (filter-out-negatives (cdr ls)))))))

In English: If the list ls is empty, return the empty list. Otherwise, if the first element on the list is negative, discard it and proceed to apply the filter to the rest of the list. But if the first element on the list is zero or positive, add it to the front of the list obtained by applying the filter to the rest of the list.


Exercise 1

Start Scheme and enter the definition of filter-out-negatives. Confirm that it correctly filters out negatives from any list of numbers. What happens if all of the elements of the list are negative? What happens if ls contains symbols instead of numbers?


Exercise 2

Write and test a Scheme procedure filter-outliers that takes a list of real numbers and filters out those that are not in the range from 0 to 100.

> (filter-outliers '(93 86 92 -3 100 81 77 84))
(93 86 92 100 81 77 84)
> (filter-outliers '(-50 0 50 100 150))
(0 50 100)
> (filter-outliers '(1000000))
()
> (filter-outliers '())
()

Recursion can be used simply to run through a series of numerical values, so that each of those values can be operated on by a fixed procedure. For instance, the following procedure uses square from the user-defined procedures lab to compute the sum of the squares of the positive integers from 1 to n, for any non-negative integer n:

(define sum-of-squares
  (lambda (n)
    (if (zero? n)
        0
        (+ (square n) (sum-of-squares (- n 1))))))

Exercise 3

Adapt this procedure so that squares of odd numbers are excluded from the sum.

Note: Exercise 3 requires the identificatin of odd numbers, and many future tasks will require a variety of other tests and operations. Thus, if you have not already done so, you may want to review some procedures built into Scheme, as defined by the Revised (5) Report on the Algorithmic Language Scheme. In particular, you may want to check that this reference is included among your bookmarks in your World-Wide Web browser. Then, you may want to move to that web page, and follow the table of contents to the bottom to the Alphabetic index of definitions of concepts, keywords, and procedures. Browsing through the list of procedures, for example, you may find some entries related to odd or even numbers. Clicking on such entries might make interesting reading.


In mathematics, a common task is to evaluate a polynomial at a particular point. For example, one might want to evaluate 3x3 + 2x2 -2x + 1 when x = 2. To accomplilsh such a task in Scheme, one might write a procedure with the x-value and a list of the coefficients. In the example, such a procedure call might have the form:


(poly-eval 2 '(3 2 -2 1))
One approach for writing this procedure is to compute the power of x in a separate power procedure and then use power within poly-eval. These pieces may be put together in the following definitions:

(define power
  (lambda (x exponent)
    (if (= exponent 0)
        1
        (if (= exponent 1)
            x
            (* x (power x (- exponent 1)))))))

(define poly-eval
  (lambda (x coeff-list)
  (if (null? coeff-list) 0
      (+ (* (car coeff-list) (power x (length (cdr coeff-list))))
         (poly-eval x (cdr coeff-list))))))

Exercise 4

  1. Check that this code produces the correct value for the example evaluating 3x3 + 2x2 -2x + 1 when x = 2.

  2. How would you use poly-eval to evaluate x5 + 5.3 when x = 1.5 ? Show the appropriate procedure call and the results obtained.

  3. The above code is incomplete, in that it does not contain comments describing what it does or explaining the main idea behind the main steps. Add appropriate comments to this code.

  4. Explain why the code for poly-eval includes the expression
    (length (cdr coeff-list)).
    Why is cdr needed here?

  5. In the above code for power, the test (= exponent 1) is unnecessary. Explain why this test is not needed, and revise the procedure to eliminate this test.

  6. In this solution, power computes the appropriate value of a power of x. The procedure poly-eval then multiplies this power by the appropriate polynomial coefficient to find the value of one term and then adds those terms together. Another approach might utilize a procedure monomial to compute an entire term. Thus, 5x3 would be evaluated at x = 2 by the call:

    
    (monomial 2 5 3)
    

    More formally, the definition of monomial would begin:

    
    (define monomial
       (lambda (x-value coefficient exponent)
          ...
       )
    )
    

    Fill in the details of this monomial procedure. Then modify poly-eval to use monomial rather than power .
    (Note: Neither monomial nor poly-eval should call power in this revised version.)


In other common applications, a numerical parameter is simply a counter, tallying the number of repetitions of some operation that may not involve numbers at all. For instance, we might want a procedure dupl that takes two arguments, a string str and a non-negative integer factor, and returns a string consisting of factor successive copies of str:

> (dupl "alf" 4)
"alfalfalfalf"
> (dupl "What? " 5)
"What? What? What? What? What? "
> (dupl "*-" 10)
"*-*-*-*-*-*-*-*-*-*-"
> (dupl "" 153)
""
> (dupl "invisible" 0)
""

Here's how we'd write it:

(define dupl
  (lambda (str factor)
    (if (zero? factor)
        ""
        (string-append str (dupl str (- factor 1))))))

The string-append operation itself deals only with strings, not with numbers; the role of factor is simply to count off the number of repetitions remaining and to cut off the recursion once this number decreases to zero.


Exercise 5

Write and test a procedure dupl-to-fit, similar to dupl, except that the second parameter indicates the maximum length permitted for the result string -- str should be duplicated as many times as possible without exceeding this maximum length. (In other words, the recursion should terminate as soon as the amount of ``free space'' left for the result string is less than the length of str.)

> (dupl-to-fit "alf" 11)
"alfalfalf"
> (dupl-to-fit "alf" 12)
"alfalfalfalf"
> (dupl-to-fit "*" 7)
"*******"
> (dupl-to-fit "*-" 7)
"*-*-*-"
> (dupl-to-fit "invisible" 3)
""

Be careful not to use the null string as the first argument to this procedure!


One thing that Scheme programmers often want to do is test whether a given value occurs as one of the elements of a given list. Using recursion, we can easily define a predicate occurs-as-element? that performs this test:

(define occurs-as-element?
  (lambda (value ls)
    (and (not (null? ls))
         (or (equal? value (car ls))
             (occurs-as-element? value (cdr ls))))))

In English: value occurs as an element of ls if (1) ls is not the empty list (since the empty list has no elements) and (2) either value is equal to the first element of ls or value is an element of the part of the ls that follows the first element.

Occurs-as-element? really is a predicate, since it always returns #t or #f specifically.

Because tests of this sort are so frequent, Scheme provides a built-in procedure member that also performs it. However, the designers of Scheme felt that it would often be useful for member to return more than a simple yes-or-no answer: When the answer is ``yes,'' member actually returns the part of ls that begins with the value sought:

> (member 'gamma '(alpha beta gamma delta epsilon))
(gamma delta epsilon)
> (member 1 '(1 2 3 4))
(1 2 3 4)
> (member "not" (list "and" "to" "by" "for"))
#f

It is as if member were defined as follows:

(define member
  (lambda (value ls)
    (cond ((null? ls) #f)
          ((equal? value (car ls)) ls)
          (else (member value (cdr ls))))))

Exercise 6

A list A is a subset of a list B if each of the elements of A occurs as an element of B. (An empty list counts as a subset of any list -- ``vacuously,'' as mathematicians say.) Define a Scheme procedure, using either member or occurs-as-element?, that takes two lists as arguments and returns #t if the first is a subset of the second, #f if it is not.


Scheme also provides the procedures memq and memv, which are exactly like member except that they compare value to each of the elements of ls by invoking eq? or eqv? rather than equal?. They are therefore slightly faster than member, but in general they are used only when it is known in advance that none of the elements of ls is a list.

Additional exercises

For more practice with recursion, look over exercises 2.12 through 2.21 (pages 54-57 in the textbook), 3.1 through 3.4 (pages 81 and 82), and 3.6 through 3.13 (pages 82-84) and try any that seem interesting. (Exercise 3.5 is not recommended at this point. It can be done, but the procedure it asks you to define can be written much more easily and naturally using other Scheme constructions that will be introduced in a couple of weeks.)


Work To Turn In:


This document is available on the World Wide Web as

http://www.math.grin.edu/~walker/courses/151.sp99/lab-more-recursion.html

created February 5, 1997 by John David Stone
last revised February 6, 1999 by Henry M. Walker

Henry M. Walker (walker@math.grin.edu)