• No results found

Designing Functions for Self-Referential Data Definitions

In document How to Design Programs (Page 142-145)

At first glance, self-referential data definitions seem to be far more complex than those for compound or mixed data. But, as the example in the preceding subsection shows, our design recipes still work. Nevertheless, in this section we discuss a new design recipe that works better for self-referential data definitions. As implied by the preceding section, the new recipe generalizes those for compound and mixed data. The new parts concern the process of discovering when a self-referential data definition is needed, deriving a template, and defining the function body:

Data Analysis and Design:

If a problem statement discusses compound information of arbitrary size, we need a recursive or self-referential data definition. At this point, we have only seen one such class, list-of-symbols, but it is easy to imagine other, yet similar classes of lists. We will get to know many other examples in this and the following part.31

For a recursive data definition to be valid, it must satisfy two conditions. First, it must contain at least two clauses. Second, at least one of the clauses must not refer back to the definition. It is good practice to identify the self-references explicitly with arrows from the references in the data definition back to its beginning.

Our running example for this section are functions that consume lists of symbols:

Template:

A self-referential data definition specifies a mixed class of data, and one of the clauses should specify a subclass of compound data. Hence the design of the template can proceed according to the recipes in sections 6.5 and 7.2.

Specifically, we formulate a cond-expression with as many cond-clauses as there are clauses in the data definition, match each recognizing condition to the corresponding clause in the data definition, and write down appropriate selector expressions in all cond-lines that process compound values.

In addition, we inspect each selector expression. For each that extracts a value of the same class of data as the input, we draw an arrow back to the function

parameter. At the end, we must have as many arrows as we have in the data definition.

Let's return to the running example. The template for a list-processing function contains a cond-expression with two clauses and one arrow:

For simplicity, this book will use a textual alternative to arrows. Instead of drawing an arrow, the templates contain self-applications of the function to the selector expression(s):

(define (fun-for-los a-list-of-symbols) (cond

[(empty? a-list-of-symbols) ...]

[else ... (first a-list-of-symbols) ...

... (fun-for-los (rest a-list-of-symbols)) ...]))

We refer to these self-applications as NATURALRECURSIONS. Body:

For the design of the body we start with those cond-lines that do not contain natural recursions. They are called BASECASES. The corresponding answers are typically easy to formulate or are already given by the examples.

Then we deal with the self-referential cases. We start by reminding ourselves what each of the expressions in the template line computes. For the recursive application we assume that the function already works as specified in our

purpose statement. The rest is then a matter of combining the various values.

Suppose we wish to define the function how-many, which determines how many symbols are on a list of symbols. Assuming we have followed the design recipe, we have the following:

;; how-many : list-of-symbols -> number

;; to determine how many symbols are on a-list-of-symbols (define (how-many a-list-of-symbols)

(cond

[(empty? a-list-of-symbols) ...]

[else ... (first a-list-of-symbols) ...

... (how-many (rest a-list-of-symbols)) ...]))

The answer for the base case is 0 because the empty list contains nothing. The two expressions in the second clause compute the first item and the number of symbols on the (rest a-list-of-symbols). To compute how many symbols there are on all of a-list-of-symbols, we just need to add 1 to the value of the latter expression:

(define (how-many a-list-of-symbols) (cond

[(empty? a-list-of-symbols) 0]

[else (+ (how-many (rest a-list-of-symbols)) 1)]))

Combining Values:

In many cases, the combination step can be expressed with Scheme's primitives, for example, +, and, or cons. If the problem statement suggests that we ask questions about the first item, we may need a nested cond-statement. Finally, in some cases, we may have to define auxiliary functions.

Figure 26 summarizes this discussion in the usual format; those design steps that we didn't discuss are performed as before. The following section discusses several examples in detail.

Phase Goal Activity Data

Analysis and Design

to formulate a

data definition develop a data definition for mixed data with at least two alternatives one alternative must not refer to the definition explicitly identify all self-references in the data definition

name the function, the classes of input data, the class of output data, and specify its purpose:

;; name : in1 in2 ...--> out

create examples of the input-output relationship make sure there is at least one example per subclass

Template to formulate an

outline develop a cond-expression with one clause per alternative add selector expressions to each clause annotate the body with natural recursions TEST: the self-references in this template and the data definition match!

Body

to define the function

formulate a Scheme expression for each simple cond-line explain for all other cond-clauses what each natural recursion computes according to the purpose statement

Test to discover mistakes (``typos'' and logic)

apply the function to the inputs of the examples check that the outputs are as predicted

Figure 26: Designing a function for self-referential data (Refines the recipes in figures 4 (pg. 5), 12 (pg. 9), and 18 (pg. 10))

9.5

In document How to Design Programs (Page 142-145)