• No results found

Exercise 1. 2 [ ] Rewrite the <datum> grammar without using the Kleene star or plus Then indicate the changes to the above derivation that are required by this revised grammar.

1.2 Recursively Specified Programs

1.2.1 Deriving Programs from BNF Data Specifications

In the previous example, we used induction on integers, so the subproblem was solved by recursively calling the procedure with a smaller value of n. When manipulating inductively defined structures, subproblems are usually solved by calling the procedure recursively on a substructure of the original.

A BNF definition for the type of data being manipulated serves as a guide both to where recursive calls should be used and to which base cases need to be handled. This is a fundamental point:

Follow the Grammar!

When defining a program based on structural induction, the structure of the program should be patterned after the structure of the data.

Typically this means that we will need one procedure for each syntactic category in the grammar. Each procedure will examine the input to see which production it corresponds to; for each

nonterminal that appears in the right-hand side, we will have a recursive call to the procedure for that nonterminal.

As an example, consider a procedure that determines whether a given list is a member of <list-of- numbers>.

A typical kind of program based on inductively defined structures is a predicate that determines whether a given value is a member of a particular set. Let us write a Scheme predicate list-of-

numbers? that takes a list and determines whether it belongs to the syntactic category <list-of-

numbers>.

> (list-of-numbers? '(1 2 3))#t> (list-of-numbers? '(1 two 3))#f> (list-of- numbers? '(1 (2) 3))#f

We can define the set of lists as

and let us recall the definition of <list-of-numbers>:

We begin by writing down the simplest behavior of the procedure: what it does when the input is the empty list.

(define list-of-

numbers? (lambda (lst) (if (null? lst) ... ...)))

By the first production in the grammar for <list-of-numbers>, the empty list is a <list-of- numbers>, so the answer should be #t.

(define list-of-

numbers? (lambda (lst) (if (null? lst)| #t ...)))

Throughout this book, bars in the left margin indicate lines that have changed since an earlier version of the same definition.

If the input is not empty, then by the grammar for <list>, it must be of the form

that is, a list whose car is a Scheme datum and whose cdr is a list. Comparing this to the grammar for <list-of-numbers>, we see that such a datum can be an element of <list-of-numbers> if and only if its car is a number and its cdr is a list-of-numbers. To find out if the cdr is a list-of- numbers, we call list-of-numbers? recursively:

To prove the correctness of list-of-numbers?, we would like to use induction on the length of lst.

1. The procedure list-of-numbers? works correctly on lists of length 0, since the only list of length 0 is the empty list, for which the correct answer, true, is returned.

2. Assuming list-of-numbers? works correctly on lists of length k, we show that it works on lists of length k + 1. Let lst be such a list. By the definition of <list-of-numbers>, lst belongs to <list-of-numbers> if and only if its car is a number and its cdr belongs to <list-of-numbers>. Since lst is of length k + 1, its cdr is of length k, so by the induction hypothesis we can determine the cdr's membership in <list-of-numbers> by passing it to list-of-numbers?. Hence list-of-numbers? correctly computes membership in <list-of-numbers> for lists of length k + 1, and the induction is complete.

The procedure terminates because every time list-of-numbers? is called, it is passed a shorter list. Every time the procedure recurs, it will be working on shorter and shorter lists, until it reaches the empty list.

Exercise 1.5 [ ] This version of list-of-numbers? works properly only when its argument is a list. Extend the definition of list-

of-numbers? so that it will work on an arbitrary Scheme <datum> and return #f on any argument that is not a list.

As a second example, we define a procedure nth-elt that takes a list lst and a zero-based index n and returns element number n of lst.

> (nth-elt '(a b c) 1)b

The procedure nth-elt does for lists what vector-ref does for vectors.

Actually, Scheme provides the procedure list-ref, which is the same as nth-elt except for error reporting, but we choose another name because standard procedures should not be tampered with unnecessarily.

When n is 0, the answer is simply the car of lst. If n is greater than 0, then the answer is element n− 1 of lst's cdr. Since neither the car nor cdr of lst exists if lst is the empty list, we must guard the car and cdr operations so that we do not take the car or cdr of an empty list.

(define nth-elt (lambda (lst n) (if (null? lst) (eopl:error 'nth-

elt "List too short by ~s elements" (+ n 1)) (if (zero? n) (car lst) (nth- elt (cdr lst) (- n 1))))))

The procedure eopl:error signals an error. Its first argument is a symbol that allows the error message to identify the procedure that called eopl:error. The second argument is a string that is then printed in the error message. There must then be an additional argument for each instance of the character sequence ~s in the string. The values of these arguments are printed in place of the corresponding ~s when the string is printed. After the error message is printed, the computation is aborted.

Let us watch how nth-elt computes its answer:

(nth-elt '(a b c d e) 3)= (nth-elt '(b c d e) 2)= (nth- elt '(c d e) 1)= (nth-elt '(d e) 0)= d

Here nth-elt recurs on shorter and shorter lists, and on smaller and smaller numbers.

If error checking were omitted, we would have to rely on car and cdr to complain about being passed the empty list, but their error messages would be less helpful. For example, if we received an error message from car, we might have to look for uses of car throughout our program. Even this would not find the error if nth-elt were provided by someone else, so that its definition was not a part of our program.

Let us try one more example of this kind before moving on to harder examples. The standard procedure length determines the number of elements in a list.

> (length '(a b c))3> (length '((x) ()))2

We write our own procedure, called list-length, to do the same thing. The length of the empty list is 0.

(define list-length (lambda (lst) (if (null? lst) 0 ...)))

The ellipsis is filled in by observing that the length of a non-empty list is one more than the length of its cdr.

(define list-

length (lambda (lst) (if (null? lst) 0| (+ 1 (list- length (cdr lst))))))

The procedures nth-elt and list-length do not check whether their arguments are of the expected type. Programs such as this that fail to check that their input is properly formed are fragile. (Users think a program is broken if it behaves badly, even when it is being used

improperly.) It is generally better to write robust programs that thoroughly check their arguments, but robust programs are often much more complicated.

The specification of a procedure should include the assumptions the procedure may make about its input, and what kinds of behavior are permitted if these assumptions fail. If a procedure is always called in a context that causes these assumptions to be satisfied, it is wasteful (and at worst

impossible) for the procedure to check its input. If the context in which the procedure will be called is unknown, then a procedure that does not check its arguments may fail in unexpected and unwelcome ways.

As we are concerned in this book with concisely conveying ideas, rather than providing general purpose tools, many of our programs are fragile. Even when programs are written solely to test ideas, some error checking may be wise to facilitate debugging.

Exercise 1.6 [ ] What happens if nth-elt and list-length are passed symbols when a list is