CSC 153 Grinnell College Spring, 2005 Computer Science Fundamentals Laboratory Exercise

# More Local Procedures

## Goals

This laboratory provides additional examples and experience with local procedures and introduces named Let expressions. The laboratory also provides practices rewriting procedures to become tail recursive.

The lab proceeds by considering several solutions to various problems, building on the previous lab on tail recursion

## Problem -- Sum: Find the sum of a list of numbers.

### Solution 1:

The first solution seems to follow a now-familiar husk-and-kernel form for tail recursion:

```
(define sum
(lambda (ls)
;Pre-condition:  ls is a list of numbers
;Post-condition:  returns the sum of the numbers in ls
(sum-kernel ls 0)
)
)
(define sum-kernel
(lambda (ls running-sum)
;Pre-condition:  ls is a list of numbers
;Post-condition:  returns the sum of the numbers in ls
(if (null? ls)
running-sum
(sum-kernel (cdr ls) (+ running-sum (car ls)))
)
)
)
```

In this approach, recursion proceeds from (1 2 3 4) toward the null list (), carrying along a running sum of values already encountered.

### Solution 2:

While Solution 1 was quite efficient, it requires a separate procedure sum-kernel. For simple exercises, such supplemental procedure definitions are unlikely to cause much trouble. When tackling complex problems, however, the number of such auxiliary procedure names can proliferate dramatically, and it can be difficult to keep track of just which names apply to what.

This problem can be solved easily by defining the sum-kernel procedure within sum. This may be done with a letrec statement:

```
(define sum
(lambda (ls)
;Pre-condition:  ls is a list of numbers
;Post-condition:  returns the sum of the numbers in ls
(letrec ((sum-kernel
(lambda (ls running-sum)
(if (null? ls)
running-sum
(sum-kernel (cdr ls) (+ running-sum (car ls)))
)
)
))
(sum-kernel ls 0)
)
)
)
```

### Solution 3:

Solution 2 is tail recursive and contains sum-kernel as a local procedure. Thus, it runs efficiently and avoids possible name conflicts with procedures defined elsewhere. Solution 2, however, seems somewhat cumbersome, since much of the body of the letrec expression seems rather formal. In particular, the lambda reference is used to identify parameters in one stage and the (sum-kernel ls 0) gives values to these parameters as part of a procedure call.

### Named Let Expressions

The named let combines this formalism in a somewhat more compact form:

```
(define sum
(lambda (lst)
;Pre-condition:  ls is a list of numbers
;Post-condition:  returns the sum of the numbers in ls
(let sum-kernel ((ls lst) (running-sum 0))
(if (null? ls)
running-sum
(sum-kernel (cdr ls) (+ running-sum (car ls)))
)
)
)
)
```

Here, the local procedure name (sum-kernel) follows immediately after let, and its parameters (ls and running-sum) are identified and initialized next. The body of the procedure follows. In such named let expressions, it is understood that the local procedure will be called with the given initial parameters, so there is no need to write out the call separately.

1. Run this procedure with several lists of numbers to check that it works correctly.

2. In this code, sum has a formal parameter lst, and this in turn is used to initialize the formal parameter ls of sum-kernel.

• What happens if ls is used as the names of both of these parameters?
• Why do you think two names were used in the above code?

In the next example, tail recursion is particularly helpful.

## Problem -- Maximum: Find the maximum value within a list of numbers

To find a maximum, there must be at least one item on the list. Otherwise, a maximum will be undefined. Thus, the base case arises when a list contains just one element.

Four solutions follow:

### Solution 1:

```
(define maximum
(lambda (ls)
;Pre-condition:  ls is a list of numbers
;Post-condition:  returns the maximum value on ls
(cond ((null? (cdr ls)) (car ls))
((< (car ls) (maximum (cdr ls))) (maximum (cdr ls)))
(else (car ls))
)
)
)
```

### Solution 2:

```
(define maximum
(lambda (ls)
;Pre-condition:  ls is a list of numbers
;Post-condition:  returns the maximum value on ls
(if (null? (cdr ls))
(car ls)
(let ((max-in-rest (maximum (cdr ls)))
(first-item (car ls)))
(if (< first-item  max-in-rest)
max-in-rest
first-item
)
)
)
)
)
```

### Solution 3:

```
(define maximum
(lambda (ls)
;Pre-condition:  ls is a list of numbers
;Post-condition:  returns the maximum value on ls
(max-kernel (car ls) (cdr ls))
)
)

(define max-kernel
(lambda (max-so-far lst)
;Pre-condition:  lst is a list of numbers
;Post-condition:  returns the maximum value on lst
(cond ((null? lst) max-so-far)
((< (car lst) max-so-far) (max-kernel max-so-far (cdr lst)))
(else (max-kernel (car lst) (cdr lst)))
)
)
)
```

### Solution 4:

```
(define maximum
(lambda (ls)
;Pre-condition:  ls is a list of numbers
;Post-condition:  returns the maximum value on ls
(letrec ((max-kernel
(lambda (max-so-far lst)
(cond ((null? lst) max-so-far)
((< (car lst) max-so-far)
(max-kernel max-so-far (cdr lst)))
(else (max-kernel (car lst) (cdr lst)))
)
)
))
(max-kernel (car ls) (cdr ls))
)
)
)
```
1. Which of these solutions are tail recursive? Briefly explain your conclusions.

2. Modify solution 4, so that max-kernel is defined locally with a named let expression.

## Problem -- Average: Find the average of the numbers within a list

Solution Outline: Since an average requires both a sum of the items and a count of the number of items present, any solution must do both tasks. A tail recursive approach to this problem uses parameters to keep a running sum and a running count of the number of items processed.

1. Write a tail recursive solution to this problem, using a kernel procedure defined within a named let expression.

## Problem -- Odds, Negatives, Sum: Find the number of even values, the number of negative values, and the sum of a list of numbers

Solution: Our procedure includes a named let expression to allow recursive processing of the list elements. As the problem asks us to keep track of three different results, we use a parameter for each in addition to the list parameter.

```
(define odd-neg-sum
(lambda (ls)
;Pre-condition:  ls is a list of numbers
;Post-condition:  returns a triple: (# odds, #negatives, sum) for ls
(let loop ((rest ls)        ;; 4 parameters: numbers to process
(number-odd 0)   ;; running counts of odd integers
(number-neg 0)   ;;    and number of negatives
(sum 0))         ;; running sum
(if (null? rest)
(list number-odd number-neg sum)  ;; base case returns result
(loop  (cdr rest)           ;; for recursion, update list
(if (odd? (car rest));; determine correct count
(+ 1 number-odd) ;;    of odd numbers
number-odd)
(if (< (car rest) 0) ;; provide correct count
(+ 1 number-neg) ;;    of negative numbers
number-neg)
(+ (car rest) sum)   ;; update running sum
)
)
)
)
)
```
1. Check that this procedure works correctly on lists of numbers by running it with several types of test cases (e.g., lists of all odds, no odds, all negatives, no negatives, some of each type of number).

## Problem -- Maximum, Minimum, and Average: Find the maximum, minimum, and average of a list of numbers

Comment: Since three results are desired, we must decide how these results should be returned. One natural approach would be to place all three results on a single list, with the maximum first, the minimum second, and the average third.

1. Write a tail recursive solution to this problem, using a single local kernel procedure defined within a named let expression. The local procedure may have as many parameters as desired.

Hint: In binding local variables within a let expression, one variable may be bound to the result of an if expression.

2. Define a procedure index that has two arguments, an item a and a list of items ls, and returns the index of a in ls, that is, the zero-based location of a in ls. If the item is not in the list, the procedure returns -1. Test your procedure on:

```
(index 3 '(1 2 3 4 5 6)) ===> 2
(index 'so '(do re mi fa so la ti do)) ===> 4
(index 'a '(b c d e)) ===> -1
(index 'cat '()) ===> -1
```

Any loop in this procedure should be coded using a named let expression.

1. The reading on strings contained husk-and-kernel procedures number-vowels and count-vowels-by-position, which count the number of vowels in a string. Rewrite number, so that count-vowels-by-position is defined locally inside:

1. define count-vowels-by-position using a letrec statement,
2. define the count-vowels-by-position loop using a named let expression.
2. The reading on input and output included procedures to compute a table of numbers and their square roots. Rewrite sqrt-table so that all helper procedures are declared locally:

1. use a letrec procedure for any local procedures,
2. define the loop using a named let expression.

## Work To Turn In:

• Commentary for steps 2, 3.
• Complete code (with full procedures and testing) for steps 4, 5, 7, 8, 9, 10.

This document is available on the World Wide Web as

```http://www.cs.grinnell.edu/~walker/courses/153.sp07/labs/lab-named-let.shtml
```

 created 7 March 1997 last revised 24 February 2007  For more information, please contact Henry M. Walker at walker@cs.grinnell.edu.