-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathchap2.texi
7362 lines (6161 loc) · 301 KB
/
chap2.texi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
@chapter Creando abstracciones con datos
@quotation
We now come to the decisive step of mathematical abstraction: we forget about
what the symbols stand for. @dots{}[The mathematician] need not be idle; there
are many operations which he may carry out with these symbols, without ever
having to look at the things they stand for.
---Hermann Weyl, @cite{The Mathematical Way of Thinking}
@end quotation
We concentrated in @ref{Chapter 1} on computational processes and on the role
of procedures in program design. We saw how to use primitive data (numbers)
and primitive operations (arithmetic operations), how to combine procedures to
form compound procedures through composition, conditionals, and the use of
parameters, and how to abstract procedures by using @code{define}. We saw that
a procedure can be regarded as a pattern for the local evolution of a process,
and we classified, reasoned about, and performed simple algorithmic analyses of
some common patterns for processes as embodied in procedures. We also saw that
higher-order procedures enhance the power of our language by enabling us to
manipulate, and thereby to reason in terms of, general methods of computation.
This is much of the essence of programming.
In this chapter we are going to look at more complex data. All the procedures
in @ref{Chapter 1} operate on simple numerical data, and simple data are not
sufficient for many of the problems we wish to address using computation.
Programs are typically designed to model complex phenomena, and more often than
not one must construct computational objects that have several parts in order
to model real-world phenomena that have several aspects. Thus, whereas our
focus in @ref{Chapter 1} was on building abstractions by combining procedures
to form compound procedures, we turn in this chapter to another key aspect of
any programming language: the means it provides for building abstractions by
combining data objects to form @newterm{compound data}.
Why do we want compound data in a programming language? For the same reasons
that we want compound procedures: to elevate the conceptual level at which we
can design our programs, to increase the modularity of our designs, and to
enhance the expressive power of our language. Just as the ability to define
procedures enables us to deal with processes at a higher conceptual level than
that of the primitive operations of the language, the ability to construct
compound data objects enables us to deal with data at a higher conceptual level
than that of the primitive data objects of the language.
Consider the task of designing a system to perform arithmetic with rational
numbers. We could imagine an operation @code{add-rat} that takes two rational
numbers and produces their sum. In terms of simple data, a rational number can
be thought of as two integers: a numerator and a denominator. Thus, we could
design a program in which each rational number would be represented by two
integers (a numerator and a denominator) and where @code{add-rat} would be
implemented by two procedures (one producing the numerator of the sum and one
producing the denominator). But this would be awkward, because we would then
need to explicitly keep track of which numerators corresponded to which
denominators. In a system intended to perform many operations on many rational
numbers, such bookkeeping details would clutter the programs substantially, to
say nothing of what they would do to our minds. It would be much better if we
could ``glue together'' a numerator and denominator to form a pair---a
@newterm{compound data object}---that our programs could manipulate in a way
that would be consistent with regarding a rational number as a single
conceptual unit.
The use of compound data also enables us to increase the modularity of our
programs. If we can manipulate rational numbers directly as objects in their
own right, then we can separate the part of our program that deals with
rational numbers per se from the details of how rational numbers may be
represented as pairs of integers. The general technique of isolating the parts
of a program that deal with how data objects are represented from the parts of
a program that deal with how data objects are used is a powerful design
methodology called @newterm{data abstraction}. We will see how data
abstraction makes programs much easier to design, maintain, and modify.
The use of compound data leads to a real increase in the expressive
power of our programming language. Consider the idea of forming a
``linear combination'' @i{a}@i{x} + @i{b}@i{y}. We might like to write
a procedure that would accept @i{a}, @i{b}, @i{x}, and @i{y} as
arguments and return the value of @i{a}@i{x} + @i{b}@i{y}. This
presents no difficulty if the arguments are to be numbers, because we
can readily define the procedure
@lisp
(define (linear-combination a b x y)
(+ (* a x) (* b y)))
@end lisp
But suppose we are not concerned only with numbers. Suppose we would like to
express, in procedural terms, the idea that one can form linear combinations
whenever addition and multiplication are defined---for rational numbers,
complex numbers, polynomials, or whatever. We could express this as a
procedure of the form
@lisp
(define (linear-combination a b x y)
(add (mul a x) (mul b y)))
@end lisp
@noindent
where @code{add} and @code{mul} are not the primitive procedures @code{+} and
@code{*} but rather more complex things that will perform the appropriate
operations for whatever kinds of data we pass in as the arguments @code{a},
@code{b}, @code{x}, and @code{y}. The key point is that the only thing
@code{linear-combination} should need to know about @code{a}, @code{b},
@code{x}, and @code{y} is that the procedures @code{add} and @code{mul} will
perform the appropriate manipulations. From the perspective of the procedure
@code{linear-combination}, it is irrelevant what @code{a}, @code{b}, @code{x},
and @code{y} are and even more irrelevant how they might happen to be
represented in terms of more primitive data. This same example shows why it is
important that our programming language provide the ability to manipulate
compound objects directly: Without this, there is no way for a procedure such
as @code{linear-combination} to pass its arguments along to @code{add} and
@code{mul} without having to know their detailed structure.@footnote{The
ability to directly manipulate procedures provides an analogous increase in the
expressive power of a programming language. For example, in section
@ref{1-3-1} we introduced the @code{sum} procedure, which takes a procedure
@code{term} as an argument and computes the sum of the values of @code{term}
over some specified interval. In order to define @code{sum}, it is crucial
that we be able to speak of a procedure such as @code{term} as an entity in its
own right, without regard for how @code{term} might be expressed with more
primitive operations. Indeed, if we did not have the notion of ``a
procedure,'' it is doubtful that we would ever even think of the possibility of
defining an operation such as @code{sum}. Moreover, insofar as performing the
summation is concerned, the details of how @code{term} may be constructed from
more primitive operations are irrelevant.}
We begin this chapter by implementing the rational-number arithmetic system
mentioned above. This will form the background for our discussion of compound
data and data abstraction. As with compound procedures, the main issue to be
addressed is that of abstraction as a technique for coping with complexity, and
we will see how data abstraction enables us to erect suitable
@newterm{abstraction barriers} between different parts of a program.
We will see that the key to forming compound data is that a programming
language should provide some kind of ``glue'' so that data objects can be
combined to form more complex data objects. There are many possible kinds of
glue. Indeed, we will discover how to form compound data using no special
``data'' operations at all, only procedures. This will further blur the
distinction between ``procedure'' and ``data,'' which was already becoming
tenuous toward the end of @ref{Chapter 1}. We will also explore some
conventional techniques for representing sequences and trees. One key idea in
dealing with compound data is the notion of @newterm{closure}---that the glue
we use for combining data objects should allow us to combine not only primitive
data objects, but compound data objects as well. Another key idea is that
compound data objects can serve as @newterm{conventional interfaces} for
combining program modules in mix-and-match ways. We illustrate some of these
ideas by presenting a simple graphics language that exploits closure.
We will then augment the representational power of our language by introducing
@newterm{symbolic expressions}---data whose elementary parts can be arbitrary
symbols rather than only numbers. We explore various alternatives for
representing sets of objects. We will find that, just as a given numerical
function can be computed by many different computational processes, there are
many ways in which a given data structure can be represented in terms of
simpler objects, and the choice of representation can have significant impact
on the time and space requirements of processes that manipulate the data. We
will investigate these ideas in the context of symbolic differentiation, the
representation of sets, and the encoding of information.
Next we will take up the problem of working with data that may be represented
differently by different parts of a program. This leads to the need to
implement @newterm{generic operations}, which must handle many different types
of data. Maintaining modularity in the presence of generic operations requires
more powerful abstraction barriers than can be erected with simple data
abstraction alone. In particular, we introduce @newterm{data-directed
programming} as a technique that allows individual data representations to be
designed in isolation and then combined @newterm{additively} (i.e., without
modification). To illustrate the power of this approach to system design, we
close the chapter by applying what we have learned to the implementation of a
package for performing symbolic arithmetic on polynomials, in which the
coefficients of the polynomials can be integers, rational numbers, complex
numbers, and even other polynomials.
@menu
* 2-1:: Introduction to Data Abstraction
* 2-2:: Hierarchical Data and the Closure Property
* 2-3:: Symbolic Data
* 2-4:: Multiple Representations for Abstract Data
* 2-5:: Systems with Generic Operations
@end menu
@node 2-1, 2-2, Chapter 2, Chapter 2
@section Introduction to Data Abstraction
In section @ref{1-1-8}, we noted that a procedure used as an element in
creating a more complex procedure could be regarded not only as a collection of
particular operations but also as a procedural abstraction. That is, the
details of how the procedure was implemented could be suppressed, and the
particular procedure itself could be replaced by any other procedure with the
same overall behavior. In other words, we could make an abstraction that would
separate the way the procedure would be used from the details of how the
procedure would be implemented in terms of more primitive procedures. The
analogous notion for compound data is called @newterm{data abstraction}. Data
abstraction is a methodology that enables us to isolate how a compound data
object is used from the details of how it is constructed from more primitive
data objects.
The basic idea of data abstraction is to structure the programs that are to use
compound data objects so that they operate on ``abstract data.'' That is, our
programs should use data in such a way as to make no assumptions about the data
that are not strictly necessary for performing the task at hand. At the same
time, a ``concrete'' data representation is defined independent of the programs
that use the data. The interface between these two parts of our system will be
a set of procedures, called @newterm{selectors} and @newterm{constructors},
that implement the abstract data in terms of the concrete representation. To
illustrate this technique, we will consider how to design a set of procedures
for manipulating rational numbers.
@menu
* 2-1-1:: Example: Arithmetic Operations for Rational Numbers
* 2-1-2:: Abstraction Barriers
* 2-1-3:: What Is Meant by Data?
* 2-1-4:: Extended Exercise: Interval Arithmetic
@end menu
@node 2-1-1, 2-1-2, 2-1, 2-1
@subsection Example: Arithmetic Operations for Rational Numbers
Suppose we want to do arithmetic with rational numbers. We want to be able to
add, subtract, multiply, and divide them and to test whether two rational
numbers are equal.
Let us begin by assuming that we already have a way of constructing a rational
number from a numerator and a denominator. We also assume that, given a
rational number, we have a way of extracting (or selecting) its numerator and
its denominator. Let us further assume that the constructor and selectors are
available as procedures:
@itemize @bullet
@item
@code{(make-rat <@var{n}> <@var{d}>)} returns therational number whose
numerator is the integer @code{<@var{n}>} and whose denominator is the integer
@code{<@var{d}>}.
@item
@code{(numer <@var{x}>)} returns the numerator of the rational number
@code{<@var{x}>}.
@item
@code{(denom <@var{x}>)} returns the denominator of the rational number
@code{<@var{x}>}.
@end itemize
We are using here a powerful strategy of synthesis: @newterm{wishful thinking}.
We haven't yet said how a rational number is represented, or how the procedures
@code{numer}, @code{denom}, and @code{make-rat} should be implemented. Even
so, if we did have these three procedures, we could then add, subtract,
multiply, divide, and test equality by using the following relations:
@example
n_1 n_2 n_1 d_2 + n_2 d_1
--- + --- = -----------------
d_1 d_2 d_1 d_2
n_1 n_2 n_1 d_2 - n_2 d_1
--- - --- = -----------------
d_1 d_2 d_1 d_2
n_1 n_2 n_1 n_2
--- * --- = -------
d_1 d_2 d_1 d_2
n_1 / d_1 n_1 d_2
--------- = -------
n_2 / d_2 d_1 n_2
n_1 n_2
--- = --- if and only if n_1 d_2 = n_2 d_1
d_1 d_2
@end example
@noindent
We can express these rules as procedures:
@lisp
(define (add-rat x y)
(make-rat (+ (* (numer x) (denom y))
(* (numer y) (denom x)))
(* (denom x) (denom y))))
(define (sub-rat x y)
(make-rat (- (* (numer x) (denom y))
(* (numer y) (denom x)))
(* (denom x) (denom y))))
(define (mul-rat x y)
(make-rat (* (numer x) (numer y))
(* (denom x) (denom y))))
(define (div-rat x y)
(make-rat (* (numer x) (denom y))
(* (denom x) (numer y))))
(define (equal-rat? x y)
(= (* (numer x) (denom y))
(* (numer y) (denom x))))
@end lisp
Now we have the operations on rational numbers defined in terms of the selector
and constructor procedures @code{numer}, @code{denom}, and @code{make-rat}.
But we haven't yet defined these. What we need is some way to glue together a
numerator and a denominator to form a rational number.
@subsubheading Pairs
To enable us to implement the concrete level of our data abstraction, our
language provides a compound structure called a @newterm{pair}, which can be
constructed with the primitive procedure @code{cons}. This procedure takes two
arguments and returns a compound data object that contains the two arguments as
parts. Given a pair, we can extract the parts using the primitive procedures
@code{car} and @code{cdr}.@footnote{The name @code{cons} stands for
``construct.'' The names @code{car} and @code{cdr} derive from the original
implementation of Lisp on the IBM 704. That machine had an addressing scheme
that allowed one to reference the ``address'' and ``decrement'' parts of a
memory location. @code{Car} stands for ``Contents of Address part of
Register'' and @code{cdr} (pronounced ``could-er'') stands for ``Contents of
Decrement part of Register.''} Thus, we can use @code{cons}, @code{car}, and
@code{cdr} as follows:
@lisp
(define x (cons 1 2))
(car x)
@i{1}
(cdr x)
@i{2}
@end lisp
Notice that a pair is a data object that can be given a name and manipulated,
just like a primitive data object. Moreover, @code{cons} can be used to form
pairs whose elements are pairs, and so on:
@lisp
(define x (cons 1 2))
(define y (cons 3 4))
(define z (cons x y))
(car (car z))
@i{1}
(car (cdr z))
@i{3}
@end lisp
In section @ref{2-2} we will see how this ability to combine pairs means that
pairs can be used as general-purpose building blocks to create all sorts of
complex data structures. The single compound-data primitive @newterm{pair},
implemented by the procedures @code{cons}, @code{car}, and @code{cdr}, is the
only glue we need. Data objects constructed from pairs are called
@newterm{list-structured} data.
@subsubheading Representing rational numbers
Pairs offer a natural way to complete the rational-number system. Simply
represent a rational number as a pair of two integers: a numerator and a
denominator. Then @code{make-rat}, @code{numer}, and @code{denom} are readily
implemented as follows:@footnote{Another way to define the selectors and
constructor is
@lisp
(define make-rat cons)
(define numer car)
(define denom cdr)
@end lisp
The first definition associates the name @code{make-rat} with the value of the
expression @code{cons}, which is the primitive procedure that constructs pairs.
Thus @code{make-rat} and @code{cons} are names for the same primitive
constructor.
Defining selectors and constructors in this way is efficient: Instead of
@code{make-rat} @emph{calling} @code{cons}, @code{make-rat} @emph{is}
@code{cons}, so there is only one procedure called, not two, when
@code{make-rat} is called. On the other hand, doing this defeats debugging
aids that trace procedure calls or put breakpoints on procedure calls: You may
want to watch @code{make-rat} being called, but you certainly don't want to
watch every call to @code{cons}.
We have chosen not to use this style of definition in this book.}
@lisp
(define (make-rat n d) (cons n d))
(define (numer x) (car x))
(define (denom x) (cdr x))
@end lisp
Also, in order to display the results of our computations, we can print
rational numbers by printing the numerator, a slash, and the
denominator:@footnote{@code{Display} is the Scheme primitive for printing data.
The Scheme primitive @code{newline} starts a new line for printing. Neither of
these procedures returns a useful value, so in the uses of @code{print-rat}
below, we show only what @code{print-rat} prints, not what the interpreter
prints as the value returned by @code{print-rat}.}
@lisp
(define (print-rat x)
(newline)
(display (numer x))
(display "/")
(display (denom x)))
@end lisp
Now we can try our rational-number procedures:
@lisp
(define one-half (make-rat 1 2))
(print-rat one-half)
@i{1/2}
(define one-third (make-rat 1 3))
(print-rat (add-rat one-half one-third))
@i{5/6}
(print-rat (mul-rat one-half one-third))
@i{1/6}
(print-rat (add-rat one-third one-third))
@i{6/9}
@end lisp
As the final example shows, our rational-number implementation does not reduce
rational numbers to lowest terms. We can remedy this by changing
@code{make-rat}. If we have a @code{gcd} procedure like the one in section
@ref{1-2-5} that produces the greatest common divisor of two integers, we can
use @code{gcd} to reduce the numerator and the denominator to lowest terms
before constructing the pair:
@lisp
(define (make-rat n d)
(let ((g (gcd n d)))
(cons (/ n g) (/ d g))))
@end lisp
Now we have
@lisp
(print-rat (add-rat one-third one-third))
@i{2/3}
@end lisp
@noindent
as desired. This modification was accomplished by changing the constructor
@code{make-rat} without changing any of the procedures (such as @code{add-rat}
and @code{mul-rat}) that implement the actual operations.
@quotation
@strong{@anchor{Exercise 2-1}Exercise 2.1:} Define a better version of
@code{make-rat} that handles both positive and negative arguments.
@code{Make-rat} should normalize the sign so that if the rational number is
positive, both the numerator and denominator are positive, and if the rational
number is negative, only the numerator is negative.
@end quotation
@node 2-1-2, 2-1-3, 2-1-1, 2-1
@subsection Abstraction Barriers
Before continuing with more examples of compound data and data abstraction, let
us consider some of the issues raised by the rational-number example. We
defined the rational-number operations in terms of a constructor
@code{make-rat} and selectors @code{numer} and @code{denom}. In general, the
underlying idea of data abstraction is to identify for each type of data object
a basic set of operations in terms of which all manipulations of data objects
of that type will be expressed, and then to use only those operations in
manipulating the data.
We can envision the structure of the rational-number system as shown in figure
@ref{Figure 2-1}. The horizontal lines represent @newterm{abstraction
barriers} that isolate different ``levels'' of the system. At each level, the
barrier separates the programs (above) that use the data abstraction from the
programs (below) that implement the data abstraction. Programs that use
rational numbers manipulate them solely in terms of the procedures supplied
``for public use'' by the rational-number package: @code{add-rat},
@code{sub-rat}, @code{mul-rat}, @code{div-rat}, and @code{equal-rat?}. These,
in turn, are implemented solely in terms of the constructor and selectors
@code{make-rat}, @code{numer}, and @code{denom}, which themselves are
implemented in terms of pairs. The details of how pairs are implemented are
irrelevant to the rest of the rational-number package so long as pairs can be
manipulated by the use of @code{cons}, @code{car}, and @code{cdr}. In effect,
procedures at each level are the interfaces that define the abstraction
barriers and connect the different levels.
@quotation
@strong{@anchor{Figure 2-1}Figure 2.1:} Data-abstraction barriers in the
rational-number package.
@example
+------------------------------------+
--------| Programs that use rational numbers |--------
+------------------------------------+
Rational numbers in promblem domain
+---------------------------+
------------| add-rat sub-rat ... |-------------
+---------------------------+
Rational numbers as numerators and denominators
+------------------------+
--------------| make-rat numer denom |--------------
+------------------------+
Rational numbers as pairs
+----------------+
------------------| cons car cdr |------------------
+----------------+
However pairs are implemented
@end example
@end quotation
This simple idea has many advantages. One advantage is that it makes programs
much easier to maintain and to modify. Any complex data structure can be
represented in a variety of ways with the primitive data structures provided by
a programming language. Of course, the choice of representation influences the
programs that operate on it; thus, if the representation were to be changed at
some later time, all such programs might have to be modified accordingly. This
task could be time-consuming and expensive in the case of large programs unless
the dependence on the representation were to be confined by design to a very
few program modules.
For example, an alternate way to address the problem of reducing rational
numbers to lowest terms is to perform the reduction whenever we access the
parts of a rational number, rather than when we construct it. This leads to
different constructor and selector procedures:
@lisp
(define (make-rat n d)
(cons n d))
(define (numer x)
(let ((g (gcd (car x) (cdr x))))
(/ (car x) g)))
(define (denom x)
(let ((g (gcd (car x) (cdr x))))
(/ (cdr x) g)))
@end lisp
The difference between this implementation and the previous one lies in when we
compute the @code{gcd}. If in our typical use of rational numbers we access
the numerators and denominators of the same rational numbers many times, it
would be preferable to compute the @code{gcd} when the rational numbers are
constructed. If not, we may be better off waiting until access time to compute
the @code{gcd}. In any case, when we change from one representation to the
other, the procedures @code{add-rat}, @code{sub-rat}, and so on do not have to
be modified at all.
Constraining the dependence on the representation to a few interface procedures
helps us design programs as well as modify them, because it allows us to
maintain the flexibility to consider alternate implementations. To continue
with our simple example, suppose we are designing a rational-number package and
we can't decide initially whether to perform the @code{gcd} at construction
time or at selection time. The data-abstraction methodology gives us a way to
defer that decision without losing the ability to make progress on the rest of
the system.
@quotation
@strong{@anchor{Exercise 2-2}Exercise 2.2:} Consider the problem of
representing line segments in a plane. Each segment is represented as a pair
of points: a starting point and an ending point. Define a constructor
@code{make-segment} and selectors @code{start-segment} and @code{end-segment}
that define the representation of segments in terms of points. Furthermore, a
point can be represented as a pair of numbers: the @i{x} coordinate and the
@i{y} coordinate. Accordingly, specify a constructor @code{make-point} and
selectors @code{x-point} and @code{y-point} that define this representation.
Finally, using your selectors and constructors, define a procedure
@code{midpoint-segment} that takes a line segment as argument and returns its
midpoint (the point whose coordinates are the average of the coordinates of the
endpoints). To try your procedures, you'll need a way to print points:
@lisp
(define (print-point p)
(newline)
(display "(")
(display (x-point p))
(display ",")
(display (y-point p))
(display ")"))
@end lisp
@end quotation
@quotation
@strong{@anchor{Exercise 2-3}Exercise 2.3:} Implement a representation for
rectangles in a plane. (Hint: You may want to make use of @ref{Exercise 2-2}.)
In terms of your constructors and selectors, create procedures that compute the
perimeter and the area of a given rectangle. Now implement a different
representation for rectangles. Can you design your system with suitable
abstraction barriers, so that the same perimeter and area procedures will work
using either representation?
@end quotation
@node 2-1-3, 2-1-4, 2-1-2, 2-1
@subsection What Is Meant by Data?
We began the rational-number implementation in section @ref{2-1-1} by
implementing the rational-number operations @code{add-rat}, @code{sub-rat}, and
so on in terms of three unspecified procedures: @code{make-rat}, @code{numer},
and @code{denom}. At that point, we could think of the operations as being
defined in terms of data objects---numerators, denominators, and rational
numbers---whose behavior was specified by the latter three procedures.
But exactly what is meant by @newterm{data}? It is not enough to say
``whatever is implemented by the given selectors and constructors.'' Clearly,
not every arbitrary set of three procedures can serve as an appropriate basis
for the rational-number implementation. We need to guarantee that, if we
construct a rational number @code{x} from a pair of integers @code{n} and
@code{d}, then extracting the @code{numer} and the @code{denom} of @code{x} and
dividing them should yield the same result as dividing @code{n} by @code{d}.
In other words, @code{make-rat}, @code{numer}, and @code{denom} must satisfy
the condition that, for any integer @code{n} and any non-zero integer @code{d},
if @code{x} is (@code{make-rat n d}), then
@example
(numer x) n
--------- = ---
(denom x) d
@end example
In fact, this is the only condition @code{make-rat}, @code{numer}, and
@code{denom} must fulfill in order to form a suitable basis for a
rational-number representation. In general, we can think of data as defined by
some collection of selectors and constructors, together with specified
conditions that these procedures must fulfill in order to be a valid
representation.@footnote{Surprisingly, this idea is very difficult to formulate
rigorously. There are two approaches to giving such a formulation. One,
pioneered by C. A. R. Hoare (1972), is known as the method of @newterm{abstract
models}. It formalizes the ``procedures plus conditions'' specification as
outlined in the rational-number example above. Note that the condition on the
rational-number representation was stated in terms of facts about integers
(equality and division). In general, abstract models define new kinds of data
objects in terms of previously defined types of data objects. Assertions about
data objects can therefore be checked by reducing them to assertions about
previously defined data objects. Another approach, introduced by Zilles at
@acronym{MIT}, by Goguen, Thatcher, Wagner, and Wright at IBM (see Thatcher,
Wagner, and Wright 1978), and by Guttag at Toronto (see Guttag 1977), is called
@newterm{algebraic specification}. It regards the ``procedures'' as elements
of an abstract algebraic system whose behavior is specified by axioms that
correspond to our ``conditions,'' and uses the techniques of abstract algebra
to check assertions about data objects. Both methods are surveyed in the paper
by Liskov and Zilles (1975).}
This point of view can serve to define not only ``high-level'' data objects,
such as rational numbers, but lower-level objects as well. Consider the notion
of a pair, which we used in order to define our rational numbers. We never
actually said what a pair was, only that the language supplied procedures
@code{cons}, @code{car}, and @code{cdr} for operating on pairs. But the only
thing we need to know about these three operations is that if we glue two
objects together using @code{cons} we can retrieve the objects using @code{car}
and @code{cdr}. That is, the operations satisfy the condition that, for any
objects @code{x} and @code{y}, if @code{z} is @code{(cons x y)} then @code{(car
z)} is @code{x} and @code{(cdr z)} is @code{y}. Indeed, we mentioned that
these three procedures are included as primitives in our language. However,
any triple of procedures that satisfies the above condition can be used as the
basis for implementing pairs. This point is illustrated strikingly by the fact
that we could implement @code{cons}, @code{car}, and @code{cdr} without using
any data structures at all but only using procedures. Here are the
definitions:
@lisp
(define (cons x y)
(define (dispatch m)
(cond ((= m 0) x)
((= m 1) y)
(else (error "Argument not 0 or 1 -- CONS" m))))
dispatch)
(define (car z) (z 0))
(define (cdr z) (z 1))
@end lisp
This use of procedures corresponds to nothing like our intuitive notion of what
data should be. Nevertheless, all we need to do to show that this is a valid
way to represent pairs is to verify that these procedures satisfy the condition
given above.
The subtle point to notice is that the value returned by @code{(cons x y)} is a
procedure---namely the internally defined procedure @code{dispatch}, which
takes one argument and returns either @code{x} or @code{y} depending on whether
the argument is 0 or 1. Correspondingly, @code{(car z)} is defined to apply
@code{z} to 0. Hence, if @code{z} is the procedure formed by @code{(cons x
y)}, then @code{z} applied to 0 will yield @code{x}. Thus, we have shown that
@code{(car (cons x y))} yields @code{x}, as desired. Similarly, @code{(cdr
(cons x y))} applies the procedure returned by @code{(cons x y)} to 1, which
returns @code{y}. Therefore, this procedural implementation of pairs is a
valid implementation, and if we access pairs using only @code{cons},
@code{car}, and @code{cdr} we cannot distinguish this implementation from one
that uses ``real'' data structures.
The point of exhibiting the procedural representation of pairs is not that our
language works this way (Scheme, and Lisp systems in general, implement pairs
directly, for efficiency reasons) but that it could work this way. The
procedural representation, although obscure, is a perfectly adequate way to
represent pairs, since it fulfills the only conditions that pairs need to
fulfill. This example also demonstrates that the ability to manipulate
procedures as objects automatically provides the ability to represent compound
data. This may seem a curiosity now, but procedural representations of data
will play a central role in our programming repertoire. This style of
programming is often called @newterm{message passing}, and we will be using it
as a basic tool in @ref{Chapter 3} when we address the issues of modeling and
simulation.
@quotation
@strong{@anchor{Exercise 2-4}Exercise 2.4:} Here is an alternative procedural
representation of pairs. For this representation, verify that @code{(car (cons
x y))} yields @code{x} for any objects @code{x} and @code{y}.
@lisp
(define (cons x y)
(lambda (m) (m x y)))
(define (car z)
(z (lambda (p q) p)))
@end lisp
What is the corresponding definition of @code{cdr}? (Hint: To verify that this
works, make use of the substitution model of section @ref{1-1-5}.)
@end quotation
@quotation
@strong{@anchor{Exercise 2-5}Exercise 2.5:} Show that we can represent pairs of
nonnegative integers using only numbers and arithmetic operations if we
represent the pair @i{a} and @i{b} as the integer that is the product 2^@i{a}
3^@i{b}. Give the corresponding definitions of the procedures @code{cons},
@code{car}, and @code{cdr}.
@end quotation
@quotation
@strong{@anchor{Exercise 2-6}Exercise 2.6:} In case representing pairs as
procedures wasn't mind-boggling enough, consider that, in a language that can
manipulate procedures, we can get by without numbers (at least insofar as
nonnegative integers are concerned) by implementing 0 and the operation of
adding 1 as
@lisp
(define zero (lambda (f) (lambda (x) x)))
(define (add-1 n)
(lambda (f) (lambda (x) (f ((n f) x)))))
@end lisp
This representation is known as @newterm{Church numerals}, after its inventor,
Alonzo Church, the logician who invented the @i{[lambda]} calculus.
Define @code{one} and @code{two} directly (not in terms of @code{zero} and
@code{add-1}). (Hint: Use substitution to evaluate @code{(add-1 zero)}). Give
a direct definition of the addition procedure @code{+} (not in terms of
repeated application of @code{add-1}).
@end quotation
@node 2-1-4, , 2-1-3, 2-1
@subsection Extended Exercise: Interval Arithmetic
Alyssa P. Hacker is designing a system to help people solve engineering
problems. One feature she wants to provide in her system is the ability to
manipulate inexact quantities (such as measured parameters of physical devices)
with known precision, so that when computations are done with such approximate
quantities the results will be numbers of known precision.
Electrical engineers will be using Alyssa's system to compute electrical
quantities. It is sometimes necessary for them to compute the value of a
parallel equivalent resistance @i{R}_@i{p} of two resistors @i{R}_1 and @i{R}_2
using the formula
@example
1
R_p = -------------
1/R_1 + 1/R_2
@end example
Resistance values are usually known only up to some tolerance guaranteed by the
manufacturer of the resistor. For example, if you buy a resistor labeled ``6.8
ohms with 10% tolerance'' you can only be sure that the resistor has a
resistance between 6.8 - 0.68 = 6.12 and 6.8 + 0.68 = 7.48 ohms. Thus, if you
have a 6.8-ohm 10% resistor in parallel with a 4.7-ohm 5% resistor, the
resistance of the combination can range from about 2.58 ohms (if the two
resistors are at the lower bounds) to about 2.97 ohms (if the two resistors are
at the upper bounds).
Alyssa's idea is to implement ``interval arithmetic'' as a set of arithmetic
operations for combining ``intervals'' (objects that represent the range of
possible values of an inexact quantity). The result of adding, subtracting,
multiplying, or dividing two intervals is itself an interval, representing the
range of the result.
Alyssa postulates the existence of an abstract object called an ``interval''
that has two endpoints: a lower bound and an upper bound. She also presumes
that, given the endpoints of an interval, she can construct the interval using
the data constructor @code{make-interval}. Alyssa first writes a procedure for
adding two intervals. She reasons that the minimum value the sum could be is
the sum of the two lower bounds and the maximum value it could be is the sum of
the two upper bounds:
@lisp
(define (add-interval x y)
(make-interval (+ (lower-bound x) (lower-bound y))
(+ (upper-bound x) (upper-bound y))))
@end lisp
Alyssa also works out the product of two intervals by finding the minimum and
the maximum of the products of the bounds and using them as the bounds of the
resulting interval. (@code{Min} and @code{max} are primitives that find the
minimum or maximum of any number of arguments.)
@lisp
(define (mul-interval x y)
(let ((p1 (* (lower-bound x) (lower-bound y)))
(p2 (* (lower-bound x) (upper-bound y)))
(p3 (* (upper-bound x) (lower-bound y)))
(p4 (* (upper-bound x) (upper-bound y))))
(make-interval (min p1 p2 p3 p4)
(max p1 p2 p3 p4))))
@end lisp
To divide two intervals, Alyssa multiplies the first by the reciprocal of the
second. Note that the bounds of the reciprocal interval are the reciprocal of
the upper bound and the reciprocal of the lower bound, in that order.
@lisp
(define (div-interval x y)
(mul-interval x
(make-interval (/ 1.0 (upper-bound y))
(/ 1.0 (lower-bound y)))))
@end lisp
@quotation
@strong{@anchor{Exercise 2-7}Exercise 2.7:} Alyssa's program is incomplete
because she has not specified the implementation of the interval abstraction.
Here is a definition of the interval constructor:
@lisp
(define (make-interval a b) (cons a b))
@end lisp
Define selectors @code{upper-bound} and @code{lower-bound} to complete the
implementation.
@end quotation
@quotation
@strong{@anchor{Exercise 2-8}Exercise 2.8:} Using reasoning analogous to
Alyssa's, describe how the difference of two intervals may be computed. Define
a corresponding subtraction procedure, called @code{sub-interval}.
@end quotation
@quotation
@strong{@anchor{Exercise 2-9}Exercise 2.9:} The @newterm{width} of an interval
is half of the difference between its upper and lower bounds. The width is a
measure of the uncertainty of the number specified by the interval. For some
arithmetic operations the width of the result of combining two intervals is a
function only of the widths of the argument intervals, whereas for others the
width of the combination is not a function of the widths of the argument
intervals. Show that the width of the sum (or difference) of two intervals is
a function only of the widths of the intervals being added (or subtracted).
Give examples to show that this is not true for multiplication or division.
@end quotation
@quotation
@strong{@anchor{Exercise 2-10}Exercise 2.10:} Ben Bitdiddle, an expert systems
programmer, looks over Alyssa's shoulder and comments that it is not clear what
it means to divide by an interval that spans zero. Modify Alyssa's code to
check for this condition and to signal an error if it occurs.
@end quotation
@quotation
@strong{@anchor{Exercise 2-11}Exercise 2.11:} In passing, Ben also cryptically
comments: ``By testing the signs of the endpoints of the intervals, it is
possible to break @code{mul-interval} into nine cases, only one of which
requires more than two multiplications.'' Rewrite this procedure using Ben's
suggestion.
After debugging her program, Alyssa shows it to a potential user, who complains
that her program solves the wrong problem. He wants a program that can deal
with numbers represented as a center value and an additive tolerance; for
example, he wants to work with intervals such as 3.5 +/- 0.15 rather than
[3.35, 3.65]. Alyssa returns to her desk and fixes this problem by supplying
an alternate constructor and alternate selectors:
@lisp
(define (make-center-width c w)
(make-interval (- c w) (+ c w)))
(define (center i)
(/ (+ (lower-bound i) (upper-bound i)) 2))
(define (width i)
(/ (- (upper-bound i) (lower-bound i)) 2))
@end lisp
Unfortunately, most of Alyssa's users are engineers. Real engineering
situations usually involve measurements with only a small uncertainty, measured
as the ratio of the width of the interval to the midpoint of the interval.
Engineers usually specify percentage tolerances on the parameters of devices,
as in the resistor specifications given earlier.
@end quotation
@quotation
@strong{@anchor{Exercise 2-12}Exercise 2.12:} Define a constructor
@code{make-center-percent} that takes a center and a percentage tolerance and
produces the desired interval. You must also define a selector @code{percent}
that produces the percentage tolerance for a given interval. The @code{center}
selector is the same as the one shown above.
@end quotation
@quotation
@strong{@anchor{Exercise 2-13}Exercise 2.13:} Show that under the assumption of
small percentage tolerances there is a simple formula for the approximate
percentage tolerance of the product of two intervals in terms of the tolerances
of the factors. You may simplify the problem by assuming that all numbers are
positive.
After considerable work, Alyssa P. Hacker delivers her finished system.
Several years later, after she has forgotten all about it, she gets a frenzied
call from an irate user, Lem E. Tweakit. It seems that Lem has noticed that
the formula for parallel resistors can be written in two algebraically
equivalent ways:
@example
R_1 R_2
---------
R_1 + R_2
@end example
@noindent
and
@example
1
-------------
1/R_1 + 1/R_2
@end example
He has written the following two programs, each of which computes the
parallel-resistors formula differently:
@lisp
(define (par1 r1 r2)
(div-interval (mul-interval r1 r2)
(add-interval r1 r2)))
(define (par2 r1 r2)
(let ((one (make-interval 1 1)))
(div-interval one
(add-interval (div-interval one r1)
(div-interval one r2)))))
@end lisp
Lem complains that Alyssa's program gives different answers for the two ways of
computing. This is a serious complaint.
@end quotation
@quotation
@strong{@anchor{Exercise 2-14}Exercise 2.14:} Demonstrate that Lem is right.
Investigate the behavior of the system on a variety of arithmetic
expressions. Make some intervals @i{A} and @i{B}, and use them in computing the
expressions @i{A}/@i{A} and @i{A}/@i{B}. You will get the most insight by
using intervals whose width is a small percentage of the center value. Examine
the results of the computation in center-percent form (see @ref{Exercise
2-12}).
@end quotation
@quotation
@strong{@anchor{Exercise 2-15}Exercise 2.15:} Eva Lu Ator, another user, has
also noticed the different intervals computed by different but algebraically
equivalent expressions. She says that a formula to compute with intervals using
Alyssa's system will produce tighter error bounds if it can be written in such
a form that no variable that represents an uncertain number is repeated. Thus,
she says, @code{par2} is a ``better'' program for parallel resistances than
@code{par1}. Is she right? Why?
@end quotation
@quotation
@strong{@anchor{Exercise 2-16}Exercise 2.16:} Explain, in general, why
equivalent algebraic expressions may lead to different answers. Can you devise
an interval-arithmetic package that does not have this shortcoming, or is this
task impossible? (Warning: This problem is very difficult.)
@end quotation
@node 2-2, 2-3, 2-1, Chapter 2
@section Hierarchical Data and the Closure Property
As we have seen, pairs provide a primitive ``glue'' that we can use to
construct compound data objects. @ref{Figure 2-2} shows a standard way to
visualize a pair---in this case, the pair formed by @code{(cons 1 2)}. In this
representation, which is called @newterm{box-and-pointer notation}, each object
is shown as a @newterm{pointer} to a box. The box for a primitive object
contains a representation of the object. For example, the box for a number
contains a numeral. The box for a pair is actually a double box, the left part
containing (a pointer to) the @code{car} of the pair and the right part
containing the @code{cdr}.
@quotation
@strong{@anchor{Figure 2-2}Figure 2.2:} Box-and-pointer representation of
@code{(cons 1 2)}.
@example
+---+---+ +---+
---->| * | *-+---->| 2 |
+-|-+---+ +---+
|