Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 60 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
60
Dung lượng
3,58 MB
Nội dung
-506- 3. In the second case, the natural recursion adds a new card to the end of a-hand . Indeed, because the given a-hand isn't the last one in the chain, the natural recursion does everything that needs to be done. Here is the complete definition of add-at-end! : ;; add-at-end! : rank suit hand -> void ;; effect: to add a card with v as rank and s as suit at the end of a- hand (define (add-at-end! rank suit a-hand) (cond [(empty? (hand-next a-hand)) (set-hand-next! a-hand (make-hand rank suit empty))] [else (add-at-end! rank suit (hand-next a-hand))])) It closely resembles the list-processing functions we designed in part II. This should come as no surprise, because add-at-end! processes values from a class that closely resembles the data definition of lists and the design recipes are formulated in a general manner. Exercise 41.3.1. Evaluate the following program by hand: (define hand0 (create-hand 13 SPADES)) (begin (add-at-end! 1 DIAMONDS hand0) (add-at-end! 2 CLUBS hand0) hand0) Test the function with this example. Make up two other examples. Recall that each example consists of an initial hand, cards to be added, and a prediction of what the result should be. Then test the function with the additional examples. Formulate the tests as boolean-valued expressions. Exercise 41.3.2. Develop the function last-card . It consumes a hand and produces a list with the last card's rank and suit. How can we use this function to test the add-at-end! function? Exercise 41.3.3. Suppose a family tree consists of structures that record the name, social security number, and parents of a person. Describing such a tree requires a structure definition: (define-struct child (name social father mother)) and a data definition: A family-tree-node (short: ftn) is either 1. false, or 2. (make-child name socsec f m) where name is a symbol, socsec is a number, and f and m are ftns. For now, we assume that everyone has a social security number and that social security numbers are unique. TEAMFLY TEAM FLY PRESENTS -507- Following our convention from part III, false represents a lack of knowledge about someone's father or mother. As we find out more information, though, we can add nodes to our family tree. Develop the function add-ftn! . It consumes a family tree a-ft , a social security number ssc , a symbol anc, and a child structure. Its effect is to modify that structure in a-ft whose social security number is ssc . If anc is 'father , it modifies the father field to contain the given child structure; otherwise, anc is the symbol 'mother and add-ftn! mutates the mother field. If the respective fields already contain a child structure, add-ftn! signals an error. Using Functions as Arguments: Instead of accepting 'father and 'mother for anc , the function could also accept one of the two structure mutators: set-child-father! or set- child-mother! . Modify add-ftn! accordingly. Exercise 41.3.4. Develop an implementation of a hand with create-hand and add-at-end! services using encapsulated state variables and function definitions. Use set! expression but no structure mutators. Not all mutator functions are as easily designed as the add-at-end! function. Indeed, in some cases things don't even work out at all. Let's consider two additional services. The first one removes the last card in a hand. Its contract and effect statement are variations on those for add- at-end! : ;; remove-last! : hand -> void ;; effect : to remove the last card in a-hand, unless it is the only one (define (remove-last! a-hand) ) The effect is restricted because a hand must always contain one card. We can also adapt the example for add-at-end! without difficulty: (define hand0 (create-hand 13 SPADES)) (begin (add-at-end! 1 DIAMONDS hand0) (add-at-end! 2 CLUBS hand0) (remove-last! hand0) (remove-last! hand0)) The resulting value is void . The effect of the computation is to return hand0 in its initial state. The template for remove-last! is the same as that for add-at-end! because both functions process the same class of values. So the next step is to analyze what effects the function must compute for each case in the template: 1. Recall that the first clause represents the case when a-hand's next field is empty. In contrast to the situation with add-at-end!, it is not clear what we need to do now. According to the effect statement, we must do one of two things: a. If a-hand is the last item in a chain that consists of more than one hand structure, it must be removed. b. If a-hand is the only structure that remove-last! consumed, the function should have no effect. TEAMFLY TEAM FLY PRESENTS -508- But we can't know whether a-hand is the last item in a long chain of hand s or the only one. We have lost knowledge that was available at the beginning of the evaluation! The analysis of the first clause suggests the use of an accumulator. We tried the natural route and discovered that knowledge is lost during an evaluation, which is the criterion for considering a switch to an accumulator-based design recipe. Once we have recognized the need for an accumulator-style function, we encapsulate the template in a local-expression and add an accumulator argument to its definition and applications: (define (remove-last! a-hand0) (local (;; accumulator (define (rem! a-hand accumulator) (cond [(empty? (hand-next a-hand)) (hand-rank a-hand) (hand-suit a-hand) ] [else (hand-rank a-hand) (hand-suit a-hand) (rem! (hand-next a-hand) accumulator ) ]))) (rem! a-hand0 ) )) The questions to ask now are what the accumulator represents and what its first value is. The best way to understand the nature of accumulators is to study why the plain structural design of remove-last! failed. Hence we return to the analysis of our first clause in the template. When rem! reaches that clause, two things should have been accomplished. First, rem! should know that a-hand is not the only hand structure in a-hand0 . Second, rem! must be enabled to remove a-hand from a-hand0 . For the first goal, rem! 's first application should be in a context where we know that a-hand0 contains more than one card. This argument suggests a cond- expression for the body of the local-expression: (cond [(empty? (hand-next a-hand)) (void)] [else (rem! a-hand0 )]) For the second goal, rem! 's accumulator argument should always represent the hand structure that precedes a-hand in a-hand0 . Then rem! can remove a-hand by modifying the predecessor's next field: (set-hand-next! accumulator empty) Now the pieces of the design puzzle fall into place. The complete definition of the function is in figure 123. The accumulator parameter is renamed to predecessor-of:a-hand to emphasize the relationship to the parameter proper. The first application of rem! in the body of the local- expression hands it the second hand structure in a-hand0 . The second argument is a-hand0 , which establishes the desired relationship. ;; remove-last! : hand -> void ;; effect : to remove the last card in a-hand0, unless it is the only one (define (remove-last! a-hand0) (local (;; predecessor-of:a-hand represents the predecessor of ;; a-hand in the a-hand0 chain (define (rem! a-hand predecessor-of:a-hand) (cond TEAMFLY TEAM FLY PRESENTS -509- [(empty? (hand-next a-hand)) (set-hand-next! predecessor-of:a-hand empty)] [else (rem! (hand-next a-hand) a-hand)]))) (cond [(empty? (hand-next a-hand0)) (void)] [else (rem! (hand-next a-hand0) a-hand0)]))) Both applications of rem! have the shape (rem! (hand-next a-hand) a-hand) Figure 123: Removing the last card It is now time to revisit the basic assumption about the card game that the cards are added to the end of a hand. When human players pick up cards, they hardly ever just add them to the end. Instead, many use some special arrangement and maintain it over the course of a game. Some arrange hands according to suits, others according to rank, and yet others according to both criteria. Let's consider an operation for inserting a card into a hand based on its rank: ;; sorted-insert! : rank suit hand -> void ;; assume: a-hand is sorted by rank, in descending order ;; effect: to add a card with r as rank and s as suit at the proper place (define (sorted-insert! r s a-hand) ) The function assumes that the given hand is already sorted. The assumption naturally holds if we always use create-hand to create a hand and sorted-insert! to insert cards. Suppose we start with the same hand as above for add-at-end! : (define hand0 (create-hand 13 SPADES)) If we evaluate (sorted-insert! 1 DIAMONDS hand0) in this context, hands0 becomes (make-hand 13 SPADES (make-hand 1 DIAMONDS empty)) If we now evaluate (sorted-insert! 2 CLUBS hand0) in addition, we get (make-hand 13 SPADES (make-hand 2 CLUBS (make-hand 1 DIAMONDS empty))) for hand0 . This value shows what it means for a chain to be sorted in descending order. As we traverse the chain, the ranks get smaller and smaller independent of what the suits are. Our next step is to analyze the template. Here is the template, adapted to our present purpose: (define (sorted-insert! r s a-hand) (cond [(empty? (hand-next a-hand)) TEAMFLY TEAM FLY PRESENTS -510- (hand-rank a-hand) (hand-suit a-hand) ] [else (hand-rank a-hand) (hand-suit a-hand) (sorted-insert! r s (hand-next a-hand)) ])) The key step of the function is to insert the new card between two cards such that first card's rank is larger than, or equal to, r and r is larger than, or equal to, the rank of the second. Because we only have two cards in the second clause, we start by formulating the answer for the second clause. The condition we just specified implies that we need a nested cond-expression: (cond [(>= (hand-rank a-hand) r (hand-rank (hand-next a-hand))) ] [else ]) The first condition expresses in Scheme what we just discussed. In particular, (hand-rank a- hand) picks the rank of the first card in a-hand and (hand-rank (hand-next a-hand)) picks the rank of the second one. The comparison determines whether the three ranks are properly ordered. Each case of this new cond-expression deserves its own analysis: 1. If (>= (hand-rank a-hand) r (hand-rank (hand-next a-hand))) is true, then the new card must go between the two cards that are currently linked. That is, the next field of a-hand must be changed to contain a new hand structure. The new structure consists of r , s , and the original value of a-hand 's next field. This yields the following elaboration of the cond-expression: 2. (cond 3. [(>= (hand-rank a-hand) r (hand-rank (hand-next a-hand))) 4. (set-hand-next! a-hand (make-hand r s (hand-next a-hand)))] 5. [else ]) 6. If (>= (hand-rank a-hand) r (hand-rank (hand-next a-hand))) is false, the new card must be inserted at some place in the rest of the chain. Of course, the natural recursion accomplishes just that, which finishes the analysis of the second clause of sorted-insert! . Putting all the pieces together yields a partial function definition: (define (sorted-insert! r s a-hand) (cond [(empty? (hand-next a-hand)) (hand-rank a-hand) (hand-suit a-hand) ] [else (cond [(>= (hand-rank a-hand) r (hand-rank (hand-next a-hand))) (set-hand-next! a-hand (make-hand r s (hand-next a-hand)))] [else (sorted-insert! r s (hand-next a-hand))])])) The only remaining gaps are now in the first clause. The difference between the first and the second cond -clause is that there is no second hand structure in the first clause so we cannot compare ranks. Still, we can compare r and (hand- rank a-hand) and compute something based on the outcome of this comparison: (cond TEAMFLY TEAM FLY PRESENTS -511- [(>= (hand-rank a-hand) r) ] [else ]) Clearly, if the comparison expression evaluates to true, the function must mutate the next field of a-hand and add a new hand structure: (cond [(>= (hand-rank a-hand) r) (set-hand-next! a-hand (make-hand r s empty))] [else ]) The problem is that we have nothing to mutate in the second clause. If r is larger than the rank of a-hand , the new card should be inserted between the predecessor of a-hand and a-hand . But that kind of situation would have been discovered by the second clause. The seeming contradiction suggests that the dots in the second clause are a response to a singular case: The dots are evaluated only if sorted-insert! consumes a rank r that is larger than all the values in the rank fields of a-hand. In that singular case, a-hand shouldn't change at all. After all, there is no way to create a descending chain of cards by mutating a-hand or any of its embedded hand structures. At first glance, we can overcome the problem with a set! expression that changes the definition of hand0 : (set! hand0 (make-hand r s hand0)) This fix doesn't work in general though, because we can't assume that we know which variable definition must be modified. Since expressions can be abstracted over values but not variables, there is also no way to abstract over hand0 in this set!-expression. A hand is an interface: 1. 'insert :: rank suit -> void ;; create-hand : rank suit -> hand ;; to create a hand from the rank and suit of a single card (define (create-hand rank suit) (local ((define-struct hand (rank suit next)) (define the-hand (make-hand rank suit empty)) ;; insert-aux! : rank suit hand -> void ;; assume: hand is sorted by rank in descending order ;; effect: to add a card with r as rank and s as suit ;; at the proper place (define (insert-aux! r s a-hand) (cond [(empty? (hand-next a-hand)) (set-hand-next! a-hand (make-hand r s empty))] [else (cond [(>= (hand-rank a-hand) r (hand-rank (hand-next a-hand))) (set-hand-next! a-hand TEAMFLY TEAM FLY PRESENTS -512- (make-hand r s (hand-next a-hand)))] [else (insert-aux! r s (hand-next a-hand))])])) ;; other services as needed (define (service-manager msg) (cond [(symbol=? msg 'insert!) (lambda (r s) (cond [(> r (hand-rank the-hand)) (set! the-hand (make-hand r s the-hand))] [else (insert-aux! r s the-hand)]))] [else (error 'managed-hand "message not understood")]))) service-manager)) Figure 124: Encapsulation and structure mutation for hands of cards We're stuck. It is impossible to define sorted-insert! , at least as specified above. The analysis suggests a remedy, however. If we introduce a single variable that always stands for the current hand structure, we can use a combination of assignments and structure mutators to insert a new card. The trick is not to let any other part of the program refer to this variable or even change it. Otherwise a simple set! won't work, as argued before. In other words, we need a state variable for each hand structure, and we need to encapsulate it in a local-expression. Figure 124 displays the complete function definition. It follows the pattern of section 39. The function itself corresponds to create-hand , though instead of producing a structure the new create-hand function produces a manager function. At this point, the manager can deal with only one message: 'insert ; all other messages are rejected. An 'insert message first checks whether the new rank is larger than the first one in the-hand , the hidden state variable. If so, the manager just changes the-hand ; if not, it uses insert-aux! , which may now assume that the new card belongs into the middle of the chain. Exercise 41.3.5. Extend the definition in figure 124 with a service for removing the first card of a given rank, even if it is the only card. Exercise 41.3.6. Extend the definition in figure 124 with a service for determining the suits of those cards in the-hand that have a given rank. The function should produce a list of suits. Exercise 41.3.7. Reformulate create-hand in figure 124 such that the manager uses a single set!-expression and sorted-insert does not use any structure mutation. Exercise 41.3.8. Recall the definition of a binary tree from section 14.2: A binary-tree (short: BT) is either 1. false or 2. (make-node soc pn lft rgt) where soc is a number, pn is a symbol, and lft and rgt are BTs. The required structure definition is TEAMFLY TEAM FLY PRESENTS -513- (define-struct node (ssn name left right)) A binary tree is a binary-search-tree if every node structure contains a social security number that is larger than all those in the left subtree and smaller than all those in the right subtree. Develop the function insert-bst! . The function consumes a name n , a social security number s , and a bst. It modifies the bst so that it contains a new node with n and s while maintaining it as a search tree. Also develop the function remove-bst! , which removes a node with a given social security number. It combines the two subtrees of the removed node by inserting all the nodes from the right tree into the left one. The discussion in this subsection and the exercises suggest that adding or removing items from linked structures is a messy task. Dealing with an item in the middle of the linked structures is best done with accumulator-style functions. Dealing with the first structure requires encapsulation and management functions. In contrast, as exercise 41.3.7 shows, a solution without mutators is much easier to produce than a solution based on structure mutation. And the case of cards and hands, which deals with at most 52 structures, is equally efficient. To decide which of the two approaches to use requires a better understanding of algorithmic analysis (see intermezzo 5) and of the language mechanisms and program design recipes for encapsulating state variables. 41.4 Extended Exercise: Moving Pictures, a Last Time In sections 6.6, 7.4, 10.3, and 21.4 we studied how to move pictures across a canvas. A picture is a list of shapes; a shape is one of several basic geometric shapes: circles, rectangles, etc. Following our most basic design principle one function per concept we first defined functions for moving basic geometric shapes, then for mixed classes of shapes, and finally for lists of shapes. Eventually we abstracted over related functions. The functions for moving basic shapes create a new shape from an existing shape. For example, the function for moving a circle consumes a circle structure and produces a new circle structure. If we think of the circle as a painting with a round frame and the canvas as a wall, however, creating a new shape for each move is inappropriate. Instead, we should change the shape's current position. Exercise 41.4.1. Turn the functions translate-circle and translate-rectangle of exercises 6.6.2 and 6.6.8, respectively, into structure-mutating functions. Adapt move-circle from section 6.6 and move-rectangle from exercise 6.6.12 so that they use these new functions. Exercise 41.4.2. Adapt the function move-picture from exercise 10.3.6 to use the structure- mutating functions from exercise 41.4.1. Exercise 41.4.3. Use Scheme's for-each function (see Help Desk) to abstract where possible in the functions of exercise 41.4.2. TEAMFLY TEAM FLY PRESENTS -514- 76 The notation (vectorof X) is analogous to (listof X) . 77 Scheme proper provides list mutators, and a Scheme programmer would use them to represent a hand as a list of cards. TEAMFLY TEAM FLY PRESENTS -515- Section 42 Equality As we mutate structures or vectors, we use words such as ``the vector now contains false in its first field'' to describe what happens. Behind those words is the idea that the vector itself stays the same even though its properties change. What this observation suggests is that there are really two notions of equality: the one we have used so far and a new one based on effects on a structure or vector. Understanding these two notions of equality is critically important for a programmer. We therefore discuss them in detail in the following two subsections. 42.1 Extensional Equality Recall the class of posn structures from part I. A posn combines two numbers; its fields are called x and y . Here are two examples: (make-posn 3 4) (make-posn 8 6) They are obviously distinct. In contrast, the following two (make-posn 12 1) (make-posn 12 1) are equal. They both contain 12 in the x -field and 1 in the y -field. More generally, we consider two structures to be equal if they contain equal components. This assumes that we know how to compare the components, but that's not surprising. It just reminds us that processing structures follows the data definition that comes with the structure definition. Philosophers refer to this notion of equality as EXTENSIONAL EQUALITY. Section 17.8 introduced extensional equality and discussed its use for building tests. As a reminder, let's consider a function for determining the extensional equality of posn structures: ;; equal-posn : posn posn -> boolean ;; to determine whether two posns are extensionally equal (define (equal-posn p1 p2) (and (= (posn-x p1) (posn-x p2)) (= (posn-y p1) (posn-y p2)))) The function consumes two posn structures, extracts their field values, and then compares the corresponding field values using = , the predicate for comparing numbers. Its organization matches that of the data definition for posn structures; its design is standard. This implies that for recursive classes of data, we naturally need recursive equality functions. Exercise 42.1.1. Develop an extensional equality function for the class of child structures from exercise 41.3.3. If ft1 and ft2 are family tree nodes, how long is the maximal abstract running time of the function? TEAMFLY TEAM FLY PRESENTS [...]... the graph contains a cycle To understand how this works in a concrete manner, let's discuss how to model simple graphs such as those in figure 85 and how to design programs that find routes through such graphs First, we need a structure definition for nodes: (define-struct node (name to) ) Y L F M A E T The name field records the name of the node, and the to field specifies to which other node it is... vectors are: 0, 3, 4, and 7 To sort the entire interval [0,5), we must insert 1, which is (vector-ref V (sub1 5)), between 0 and 3 A E T In short, the design of in-place-sort follows the same pattern as that of the function sort in section 12.2 up to this point For sort, we also designed the main function only to find out that we needed to design an auxiliary function for inserting one more item into... 1 to each vector field: ;; increment-vec-rl : (vector number) -> void ;; effect: to increment each item in V by 1 (define (increment-vec-rl V) (for-interval (sub1 (vector-length V)) zero? sub1 (lambda (i) (vector-set! V i (+ (vector-ref V i) 1))))) A E T It processes the interval [0,(sub1 (vector-length V))], where the left boundary is determined by zero?, the termination test The starting point, however,... items in vector into the adjacent field to the left, except for the first item, which moves to the last field; 2 insert-i-j, which moves all items between two indices i and j to the right, except for the right-most one, which gets inserted into the i-th field (cmp figure 128); 3 vector-reverse!, which swaps the left half of a vector with its right half; 4 find-new-right, that is, an alternative to the definition... PRESENTS [else (vector-ref V (sub1 i)) (sort-aux V (sub1 i)) ]))) (sort-aux V (vector-length V)))) Following the design ideas of intermezzo 5, the auxiliary function consumes a natural number and uses it as an index into the vector Because the initial argument is (vector-length V), the accessible index is always (sub1 i) Recall that the key to designing functions such as sort-aux is to formulate a rigorous... N (vectorof number) -> void ;; to place the value in the i-th into its proper place ;; in the [0,i] segement of V (define (insert i V) (cond [(zero? i) (void)] [else (cond [(> (vector-ref V (sub1 i)) (vector-ref V i)) (begin (swap V (- i 1) i) (insert (sub1 i) V))] [else (void)])])) Y L F M ;; swap : (vectorof X) N N void (define (swap V i j) (local ((define temp (vector-ref V i))) (begin (vector-set!... qsort eventually encounters the empty interval and stops After qsort has sorted each fragment, there is nothing left to do; the partitioning process has arranged the vector into fragments of ascending order A E T Here is the definition of qsort, an in-place sorting algorithm for vectors: ;; qsort : (vectorof number) -> (vectorof number) ;; effect: to modify V such that it contains the same items as... right)))])) The main function's input is a vector, so it uses an auxiliary function to do its job As suggested above, the auxiliary function consumes the vector and two boundaries Each boundary is an -525TEAM FLY PRESENTS index into the vector Initially, the boundaries are 0 and (sub1 (vector-length V)), which means that qsort-aux is to sort the entire vector The definition of qsort-aux closely follows... applied to a vector's length, it should sort the entire vector This statement implies that if the first argument is less than the vector's length only some initial segment of the vector is sorted: A E T (local ((define v1 (vector 7 3 0 4 1))) (begin (sort-aux v1 4) (equal? v1 (vector 0 3 4 7 1)))) In this particular example, the last number remains in its original place, and only the first four vector... vector mutation, we can also design a function that changes the vector so that it contains the same items as before, in a sorted order Such a function is called an IN-PLACE SORT because it leaves all the items inside the existing vector A E T An in-place-sort function relies exclusively on effects on its input vector to accomplish its task: ;; in-place-sort : (vectorof number) -> void ;; effect: to . accumulator ) ]))) (rem! a-hand0 ) )) The questions to ask now are what the accumulator represents and what its first value is. The best way to understand the nature of accumulators is to study. accumulator empty) Now the pieces of the design puzzle fall into place. The complete definition of the function is in figure 123. The accumulator parameter is renamed to predecessor-of:a-hand to. order. An analogous function for vectors consumes a vector and produces a new vector. But, using vector mutation, we can also design a function that changes the vector so that it contains the same