CS 61A Fall 2000 Midterm 3 solutions 1. Pi-approximation stream (a) numerators {2 4 4 6 6 8 ...} implicitly "Implicit definition" really means not only that no procedures are defined, but also that the stream itself is used in the definition, so the answer we expected was (define numerator (cons-stream 2 (cons-stream 4 (stream-map (lambda (x) (+ x 2)) numerator)))) This is slightly different from most of the examples in the book because it needs two pump-priming cons-streams in order to get the special case of only one 2 at the beginning right. But since we didn't explicitly require using the stream itself in the definition, we also accepted (define numerator (cons-stream 2 (interleave (stream-cdr evens) (stream-cdr evens)))) where EVENS is the stream {2 4 6 8 10 ...} that can be generated in various ways, such as (scale-stream integers 2). A truly implicit (self-referential) definition with only one cons-stream can be made by generating the helper stream {2 0 2 0 2 0 ...} in one of several possible ways and adding it to the numerator stream: (define numerator (cons-stream 2 (add-streams numerator (interleave (scale-stream ones 2) (scale-stream ones 0))))) Scoring: Two points for any of the above (and correct variants). One point for a correct computation that didn't follow the pattern (starting with cons-stream). No points for solutions using recursive procedures or ones that generate incorrect streams. The most common incorrect solution was (define numerator ;; WRONG! (cons-stream 2 (interleave (stream-map (lambda (x) (+ x 2)) numerator) (stream-map (lambda (x) (+ x 2)) numerator)))) This was really well-motivated, and it's too bad it doesn't work, but you really want to add 2 only to half the elements of the stream! In fact this generates {2 4 4 6 6 6 6 8 8 8 8 8 8 8 8 10 10 10 10 10 ...} with twice as many copies of each larger number. (b) Denominator from numerator The solution we expected was (define denominator (stream-map (lambda (x) (- x 1)) (stream-cdr numerator))) The stream-cdr is needed because numerator has that single 2 at the beginning, whereas denominator is all pairs of equal values. An equivalent solution was (define denominator (stream-map - (stream-cdr numerator) ones)) A minor variation was to account for that beginning 2 not by removing it but by turning it into a pair of equal values: (define denominator (stream-map (lambda (x) (+ x 1)) (cons-stream 2 numerator))) Note that we now have to add one instead of subtracting one. Another elegant solution adds the stream {1 -1 1 -1 1 -1 ...} to numerator, avoiding the need to add or remove an element: (define denominator (stream-map + numerator (stream-map (lambda (x) (if (even? x) -1 1)) integers))) Scoring: Two points for any of the above (and correct variants). One point for a correct solution that violated the given pattern by leaving out stream-map, or for a solution that was correct except for adding 1 instead of subtracting or vice versa. No points for a solution that doesn't use numerator. (c) Combining the two. The only really good solution was (define pi/4-stream (series-stream * (stream-map / numerator denominator))) Since each term in the approximation stream is the product of the previous term and a new value, the combiner really must be *. And the new value is formed by dividing an element of numerator by the corresponding element of denominator. An okay but silly variation was to use (lambda (a b) (/ a b)) instead of just /. Did you think the word LAMBDA has to appear in the first argument to stream-map? The argument has to be a procedure, but any expression whose value is a procedure can be used. Scoring: This was basically right or wrong. There were a few correct but overly complicated variations on the above solution, which got two points; incorrect solutions got no points. A typical wrong solution was one that tried to do the job of series-stream in the argument to series-stream (multiplying elements of numerator and denominator). 2. Parallelism in real life. For this question we got several essays trying to justify solutions other than the obvious ones. For the most part we didn't accept these -- the problem did say "the answer which BEST describes..." -- with one exception noted in part (b) below. (a) People at tables can't get food; people waiting for food can't get tables. This is a potential deadlock, and that was the answer we wanted. It's true that the deadlock might not happen, because some people at tables may actually have food and might eventually leave, letting the line continue moving. It depends partly on whether the restaurant servers believe you when you say "it's okay to serve me because my friend is already sitting at a table and saving me a seat." Even if they do, a deadlock can happen if someone comes in alone (so doesn't have a friend to grab a table) and when that person gets to the front of the line, all the tables are filled with friends of people behind him/her in line. You may think "it's UNFAIR for people who don't have food yet to hog the tables" or "it's INEFFICIENT for people to sit at tables when they don't have food yet." But these are technical terms about parallellism errors, and the particular situation described in the problem is technically a deadlock. (b) There are several inspectors serving several lines in parallel, but there's only one metal detector, which acts as a single serializer for all the inspectors. This is inefficiency (too much serialization). Some people argued that the metal detector is best viewed as an actual resource, not as a serializer, and so this is correct, if unfortunate, serialization of a shared resource. We accepted that reasoning, so we also accepted "none of the above" as a correct answer. (c) You write while your friend researches. This is correct parallelism, with two processors carrying out parallelizable tasks. Some people argued that the tasks *aren't* correctly parallellizable because the research has to come before the writing. That idea has some merit for a short paper, but you wouldn't be writing a short paper as a team anyway; it'd be an individual assignment. We meant that you were writing one part of the paper while your friend was researching a different part of the paper. Some people said "unfairness" because you have to do the boring writing while your friend is off doing the interesting research. One of the nice things about working in groups is that sometimes you're lucky and your interests complement those of your friend! But even if you would feel an unfairness in this arrangement, it's not unfair in the technical sense -- a parallelism unfairness would be if the librarians always get books for your friend before they get books for you. Scoring: One point each. 3. Marbles with a class variable The key point here is that the list of colors is shared by all the marbles; it isn't a list per marble. Therefore, the LET that creates the variable must come outside the LAMBDA that creates the class. Choices 1 and 3 have the LET inside that LAMBDA, but outside the LAMBDA that represents an instance, so it would be creating an instance variable; each marble would list only its own color. What distinguishes answers 2 and 4 is the position of the SET! that adds a particular marble's color to the list of colors. Should that happen when the marble (instance) is created, or when it's invoked? Clearly when it's created. So the SET! has to be inside the class LAMBDA, but not inside the instance LAMBDA. This is the case in answer 4 (and also in answer 3, but that was ruled out in the previous paragraph). Scoring: One point for choosing (only) the fourth definition. 4. Parallel marriages. We apologize for two errors in the procedures we gave you. In part (a) we got the order of arguments to insert-queue wrong; it should have been (define (vegas-goers man woman) (insert-queue! man-queue man) (insert-queue! woman-queue woman)) In part (b) we put the local variables GROOM and BRIDE inside an internal procedure and then used them outside of that procedure. We should have said (define (justice name) (let ((groom #F) (bride #F)) ((s (lambda () (set! groom (front-queue man-queue)) (set! bride (front-queue woman-queue)) (delete-queue! man-queue) (delete-queue! woman-queue)))) (marry groom bride) (display "Congratulations!"))) (a) This is indeed dangerous, because we could get incorrect results. INSERT-QUEUE! needs to be serialized. For example, if the queue is empty, and two couples try to insert themselves at the same time, they might both get #T from (empty-queue? queue) and therefore both try to make themsevles the first element. One of them would end up not queued at all. Even if the queue isn't empty, the SET-CDR! and SET-REAR-PTR! calls near the end of INSERT-QUEUE! must be done atomically, or else a queue entry will be lost. By the way, the danger is that someone will be dropped from the queue completely, not that people will be added out of order. So Paul might end up marrying Yoko, but then Linda won't have anyone to marry; she can't marry John if Paul marries Yoko. There can't be a deadlock if there is no serialization! That answer couldn't be right. (b) Although there was an error in our code, it wasn't an error in parallelism. The sentence "Because queues, not justices..." makes the "This is incorrect" choice wrong despite our bug. If the queues did the serialization, nothing would prevent justice A getting groom #1, then justice B getting groom #2 and bride #1, and then justice A getting bride #2. The processing of both queues must be a single atomic transaction, as in Alyssa's program, which is correct in its handling of serialization. Scoring: one point each. 5. Implementing sorted lists. The fact that the lists are sorted really doesn't affect the heart of the problem, which is about our ability to modify a data structure. In effect this question is asking why SICP's tables are represented using a header of *TABLE* as a first element even when the table is empty. (a) Can we represent an empty data structure using the empty list? No, we can't; there is nothing for INSERT! to mutate when inserting the first element. (This is the second of the answers given.) Why can't we say (define (insert! element slist) ;; WRONG! (if (null? slist) (set! slist (cons element '())) ...)) The SET! inside INSERT! changes INSERT!'s local variable SLIST, but doesn't affect the global variables SLIST1 or SLIST2 in the examples. So the result of (insert! 1 slist1) would indeed be to create a list containing 1 as its only element, but that list would *not* be pointed to by the variable SLIST1, which still points to the empty list. This is a way in which SET! and SET-CAR!/SET-CDR! are different. SET! can only change the specific variable used as its first argument -- in this example, the variable SLIST that's local to INSERT!. But if a global and a local variable point to the same pair, then the procedure can use SET-CAR! or SET-CDR! on (the pair pointed to by) its local variable, and the mutation will affect the value as seen by the global variable. (b) Okay, so we need a header pair to represent an empty sorted list, so that the first insertion will be possible. So let's make one: (define the-empty-sorted-list ;; WRONG! (cons 'sorted-list '())) Why is this wrong? With this implementation, SLIST1 and SLIST2 are the same pair, the one made when we defined the-empty-sorted-list. So any insertions made to either one will affect the other -- they share the same memory, as the third answer points out. The correct solution is to construct a new pair for each new sorted list: (define (make-sorted-list) (cons 'sorted-list '())) (define slist1 (make-sorted-list)) (define slist2 (make-sorted-list)) Here we call a constructor procedure for each of the sorted lists; the procedure makes a new pair to serve as the header for each list. It's because of the need for a header pair that the display procedure SORTED->REGULAR is needed; this procedure just removes the header for display purposes: (define (sorted->regular slist) (cdr slist)) Scoring: Two points each, all or nothing. Group question: environment diagram The first DEFINE is an abbreviation for (define bar (lambda (x) (let ...))) So the implicit LAMBDA expression creates a procedure P1: param=x, body=(let ...), env=Global It's in the global environment because that's always the current environment for expressions typed at the Scheme prompt, as opposed to expressions inside a procedure body. Then in the global environment we bind BAR to this: G: BAR -> P1 That's all that happens when Scheme evaluates the first expression; the stuff in the procedure body is not evaluated at this time. The second expression is (define foo (bar 4)). First we have to evaluate the procedure call (bar 4). We do this by creating a new environment in which BAR's parameter (x) is bound to the argument value (4), extending the procedure's defining environment: E1: X -> 4, extends G Using E1 as the current environment, we evaluate the procedure body, which is the expression (let ((z (lambda (b) ...)) (c x)) (lambda (x) ...)) LET abbreviates a lambda and a procedure call, as follows: ( (lambda (z c) (lambda (x) ...)) (lambda (b) ...) x ) ------------------------------- ---------------- - implicit procedure arg for z c To evaluate a procedure call, we first evaluate all the subexpressions. Two of them are LAMBDA expressions, which create procedures: P2: params=z,c, body=(lambda (x) ...), env=E1 P3: param=b, body=(* x b), env=E1 These procedures have E1 as their defining environment because that's the current environment right now. The third expression, X, is evaluated by looking for the symbol X in E1. We find it; its value is 4. Now that we've evaluated the subexpressions, we call procedure P2 with arguments P3 and 4. We do this by creating a new environment binding P2's parameters to these arguments and extending P2's defining environment: E2: Z -> P3, C -> 4, extends E1 Note that the value of C is 4, not X! Parameters are bound to the actual argument *values*, not to the expressions that give rise to the values. Similarly, the value of Z is P3, not (* X B) or (LAMBDA (B) (* X B)). With E2 as the current environment, we now evaluate the body of P2. That body is a lambda expression, so it creates a procedure: P4: param=x, body=(set! ...)..., env=E2 P4 is the value of the lambda expression, so it's the value returned by the call to P2, so it's the value returned by the LET, so it's the value returned by (bar 4), and so back in the global environment we bind FOO to this value: G: BAR -> P1, FOO -> P4 That's it for the second expression. We invoked P2 because it was created by a LET, which both creates and invokes a procedure, but nothing says to invoke P3 or P4 so far. Now for the third expression, (FOO 3). This is a procedure call; we evaluate the subexpressions (looking up FOO in the global environment and noticing that 3 is self-evaluating). Then we invoke P4, which is the value of FOO, with 3 as its argument. This creates a new environment: E3: X -> 3, extends E2 With E3 as the current environment, we evaluate the body of P4. This body contains two expressions. The first is (set! c (z (* c x))) To evaluate this, we must first evaluate (z (* c x)), which is a procedure call. We evaluate the subexpression Z by looking it up in E3, the current environment. We don't find it directly in E3, but E3 extends E2, where Z is bound to P3. The subexpression (* c x) is a procedure call. We evaluate the subexpressions, finding * in G (it's bound to the primitive multiply procedure), C in E2 (where it's bound to 4), and X in E3 (bound to 3). Since the multiply procedure is primitive, we apply it by magic, getting the answer 12 without creating a new environment. This value 12 is the argument to Z (which is actually P3). So we create a new environment for the call to P3: E4: B -> 12, extends E1 The body of P3 is (* x b). We look up these three symbols and find * in the global environment (bound to the primitive multiplication procedure), X in E1 (not in E3, which isn't part of the current environment!) (bound in E1 to 4), and B in E4, bound to 12. By magic we multiply 4 by 12 and get the answer 48. We did that to evaluate the second argument to (set! c ...). Now that we know the argument value, we can find a binding for C in what's now the current environment, the one in which we saw the SET! expression, namely E3. There's no binding for C in E3's first frame, but we find one in E2, which E3 extends. There we change C's binding from 4 to 48. The second and last expression in the body of P4 is just C, so we return the value 48 that we just put there. To summarize, the diagram has five frames (including G) and four procedures: G: BAR -> P1, FOO -> P4 E1: X -> 4, extends G E2: Z -> P3, C -> 4, extends E1 E3: X -> 3, extends E2 E4: B -> 12, extends E1 P1: param=x, body=(let ...), env=Global P2: params=z,c, body=(lambda (x) ...), env=E1 P3: param=b, body=(* x b), env=E1 P4: param=x, body=(set! ...)..., env=E2 Common errors: The most common was having P3's right bubble point to E2 instead of E1, probably from thinking that the lambda expression was within the scope of the LET. But the expressions that provide values for the LET variables must be computed before the LET variables are bound! This generally led to having E4 extend E2 instead of E1. Several people left out E4 altogether. One mistake I couldn't understand was that a lot of people had FOO bound to E2 instead of P4. An environment can't be the value of a variable! (Environments are not first class in Scheme.) As mentioned above, too many people bound the E2 variables to expressions [C -> X, Z -> (lambda (b) (* x b))] rather than to the values of those expressions. (In effect you are inventing normal order evaluation, which isn't what Scheme does.) Some people bound B to 16 instead of 12, thinking that X was 4 (from E1) instead of the correct 3 (from E3). This mistake wasn't necessarily accompanied by any error in the arrangement of E1 and E3 themselves. These people generally got 64 as the answer to (foo 3). Some people had E1 and E2 backwards (that is, E1 extending E2 which extends G). I can't imagine a mental model that would produce such a diagram, since the value of C in E2 is based on the value of X in E1. Perhaps these groups wanted to evaluate the LET as part of the process of defining BAR (rather than when BAR is invoked), but in that case it's not clear to me where the value of C came from. A few people had E4 extending E3 (dynamic scope), but not as many as I would have predicted -- it was more common to omit E4 altogether. Some people added empty frames to the diagram. An empty frame is created by invoking a procedure of no arguments [(lambda () ...)], but there aren't any of those in this problem. Some people added bindings for Z and C to E1 instead of creating a separate E2. That would be appropriate for internal definitions: (define (bar x) (define (z b) (* x b)) (define c x) (lambda (x) ...)) But LET creates a new frame because it abbreviates a LAMBDA and an invocation of the resulting procedure. Scoring: 3 if correct. 2 for a single error (which could involve two pointers if a procedure pointing to the wrong environment led to another environment extending that wrong environment. 1 for multiple errors in an otherwise reasonable environment diagram. 0 for multiple missing frames or seriously incoherent results such as variables whose value is an environment. ----------------------------------- If you don't like your grade, first check these solutions. If we graded your paper according to the standards shown here, and you think the standards were wrong, too bad -- we're not going to grade you differently from everyone else. If you think your paper was not graded according to these standards, bring it to Brian or your TA. We will regrade the entire exam carefully; we may find errors that we missed the first time around. :-) If you believe our solutions are incorrect, we'll be happy to discuss it, but you're probably wrong!