The HTML for this page has been automatically generated by parsing the loosely formatted source code & comments included in my solutions repository (https://github.com/zv/SICP-guile), if you encounter any unformatted solutions, you think you’ve spotted an error or you have something you’d like to share about the exercises, don’t hesitate to leave a comment below (or a pull request on the source repo).
Exercises Chapter 3
An accumulator is a procedure that is called repeatedly with a single numeric argument and accumulates its arguments into a sum. Each time it is called, it returns the currently accumulated sum. Write a procedure make-accumulator that generates accumulators, each maintaining an independent sum. The input to make-accumulator should specify the initial value of the sum;
(define A (make-accumulator 5))
(define (make-accumulator x) (lambda (n) (+ x n)))
In software-testing applications, it is useful to be able to count the number of times a given procedure is called during the course of a computation. Write a procedure make-monitored that takes as input a procedure, f, that itself takes one input. The result returned by make-monitored is a third procedure, say mf, that keeps track of the number of times it has been called by maintaining an internal counter. If the input to mf is the special symbol how-many-calls?, then mf returns the value of the counter. If the input is the special symbol reset-count, then mf resets the counter to zero. For any other input, mf returns the result of calling f on that input and increments the counter. For instance, we could make a monitored version of the sqrt procedure:
(define (make-monitored f) (define calls 0) (lambda (n) (if (eq? n 'how-many-calls?) calls (begin (set! calls (inc calls)) (f n)))))
Modify the make-account procedure so that it creates password-protected accounts. That is, make-account should take a symbol as an additional argument, as in
(define 3.3/acc (make-account 100 'secret-password)) (define (3.3/make-account acct# passwd) (define (withdraw amount) (if (>= balance amount) (begin (set! balance (- balance amount)) balance) "Insufficient funds")) (define (deposit amount) (set! balance (+ balance amount)) balance) (define (dispatch m) (cond ((eq? m 'withdraw) withdraw) ((eq? m 'deposit) deposit) (else (error "Unknown request: MAKE-ACCOUNT" m)))) dispatch)
Monte Carlo integration is a method of estimating definite integrals by means of Monte Carlo simulation. Consider computing the area of a region of space described by a predicate p ( x , y ) that is true for points ( x , y ) in the region and #f for points not in the region. For example, the region contained within a circle of radius 3 centered at (5, 7) is described by the predicate that tests whether ( x − 5 ) 2 + ( y − 7 ) 2 ≤ 3 2 . To estimate the area of the region described by such a predicate, begin by choosing a rectangle that contains the region. For example, a rectangle with diagonally opposite corners at (2, 4) and (8, 10) contains the circle above. The desired integral is the area of that portion of the rectangle that lies in the region. We can estimate the integral by picking, at random, points ( x , y ) that lie in the rectangle, and testing p ( x , y ) for each point to determine whether the point lies in the region. If we try this with many points, then the fraction of points that fall in the region should give an estimate of the proportion of the rectangle that lies in the region. Hence, multiplying this fraction by the area of the entire rectangle should produce an estimate of the integral.
Implement Monte Carlo integration as a procedure estimate-integral that takes as arguments a predicate p, upper and lower bounds x1, x2, y1, and y2 for the rectangle, and the number of trials to perform in order to produce the estimate. Your procedure should use the same monte-carlo procedure that was used above to estimate π . Use your estimate-integral to produce an estimate of π by measuring the area of a unit circle.
You will find it useful to have a procedure that returns a number chosen at random from a given range. The following random-in-range procedure implements this in terms of the random procedure used in 1.2.6, which returns a nonnegative number less than its input.136
(define (random-in-range low high) ;; had to write my own (+ low (random high))) (define (estimate-integral p x1 x2 y1 y2 trials) (define width (abs (- x2 x1))) (define height (abs (- y2 y1))) (define area (* width height)) (define (iter remaining passed) (let* ((x (random-in-range x1 x2)) (y (random-in-range y1 y2)) (is-contained? (p x y))) (cond ((= remaining 0) (/ passed trials)) (is-contained? (iter (dec remaining) (inc passed))) (else (iter (dec remaining) passed))))) (* area (iter trials 0))) (define (unit-circle-pred x y) (<= (+ (* x x) (* y y)) 1))
It is useful to be able to reset a random-number generator to produce a sequence starting from a given value. Design a new rand procedure that is called with an argument that is either the symbol generate or the symbol reset and behaves as follows: (rand ’generate) produces a new random number; ((rand ’reset) ⟨new-value⟩) resets the internal state variable to the designated ⟨new-value⟩. Thus, by resetting the state, one can generate repeatable sequences. These are very handy to have when testing and debugging programs that use random numbers.
;; This is what I assume he meant?? (define (rand command) (case command ('generate (random 10)) (else (λ (new) (seed->random-state new))))) ;; Utilities (define (count-pairs x) (if (not (pair? x)) 0 (+ (count-pairs (car x)) (count-pairs (cdr x)) 1)))
The following procedure for appending lists was introduced in 2.2.1:
(define (append x y) (if (null? x) y (cons (car x) (append (cdr x) y))))
Append forms a new list by successively consing the elements of x onto y. The procedure append! is similar to append, but it is a mutator rather than a constructor. It appends the lists by splicing them together, modifying the final pair of x so that its cdr is now y. (It is an error to call append! with an empty x.)
An infinite loop occurs (a cycle in the linked list has been made)
(define (last-pair x) (if (null? (cdr x)) x (last-pair (cdr x)))) (define x (list 'a 'b)) (define y (list 'c 'd)) (define z (append x y)) ;; Exercise 3.13 ;; What happens if we try to compute (last-pair z)? (define (make-cycle x) (set-cdr! (last-pair x) x) x)
The following procedure is quite useful, although obscure:
(define (mystery x) (define (loop x y) (if (null? x) y (let ((temp (cdr x))) (set-cdr! x y) (loop temp x)))) (loop x ’()))
Loop uses the “temporary” variable temp to hold the old value of the cdr of x, since the set-cdr! on the next line destroys the cdr. Explain what mystery does in general. Suppose v is defined by (define v (list ’a ’b ’c ’d)). Draw the box-and-pointer diagram that represents the list to which v is bound. Suppose that we now evaluate (define w (mystery v)). Draw box-and-pointer diagrams that show the structures v and w after evaluating this expression. What would be printed as the values of v and w?
Mystery requotes an array “in-place”
Ben Bitdiddle decides to write a procedure to count the number of pairs in any list structure. “It’s easy,” he reasons. “The number of pairs in any structure is the number in the car plus the number in the cdr plus one more to count the current pair.” So Ben writes the following procedure:
(define (count-pairs x) (if (not (pair? x)) 0 (+ (count-pairs (car x)) (count-pairs (cdr x)) 1)))
Show that this procedure is not correct. In particular, draw box-and-pointer diagrams representing list structures made up of exactly three pairs for which Ben’s procedure would return 3; return 4; return 7; never return at all.
(define count-three-pairs '(a b c)) (define count-four-pairs '(a b c)) (define count-seven-pairs '(a b c)) (set-car! (cdr count-four-pairs) (cdr (cdr count-four-pairs))) (set-car! count-seven-pairs (cdr count-seven-pairs)) #| Answer: (count-pairs count-three-pairs) => 3 (count-pairs count-four-pairs) => 4 (count-pairs count-seven-pairs) => 7 |#
Devise a correct version of the count-pairs procedure of Exercise 3.16 that returns the number of distinct pairs in any structure.
(Hint: Traverse the structure, maintaining an auxiliary data structure that is used to keep track of which pairs have already been counted.)
(define (zv-count-pairs xs) (define counted '()) (define (loop xs) (cond ((not (pair? xs)) 1) ((null? xs) 0) ((memq (car xs) counted) 0) (else (begin (set! counted (cons (car xs) counted)) (+ (loop (car xs)) (loop (cdr xs))))))) (loop xs))
Write a procedure that examines a list and determines whether it contains a cycle, that is, whether a program that tried to find the end of the list by taking successive cdrs would go into an infinite loop. Exercise 3.13 constructed such lists.
(define (has-cycles? xs) (define visited '()) (define (search ys) (cond ((null? ys) #f) ((memq (car ys) visited) #t) (else (begin (set! visited (cons (car ys) visited)) (search (cdr ys)))))) (search xs))
Redo Exercise 3.18 using an algorithm that takes only a constant amount of space. (This requires a very clever idea.)
(define* (linear-cycle-search x1 #:optional (x2 (cdr x1))) (cond ((or (null? (cdr x1)) (null? (cdr x2))) #f) ((eq? x1 x2) #t) (else (linear-cycle-search (cdr x1) (cdr (cdr x2))))))
Ben Bitdiddle decides to test the queue implementation described above. He types in the procedures to the Lisp interpreter and proceeds to try them out:
“It’s all wrong!” he complains. “The interpreter’s response shows that the last item is inserted into the queue twice. And when I delete both items, the second b is still there, so the queue isn’t empty, even though it’s supposed to be.” Eva Lu Ator suggests that Ben has misunderstood what is happening. “It’s not that the items are going into the queue twice,” she explains. “It’s just that the standard Lisp printer doesn’t know how to make sense of the queue representation. If you want to see the queue printed correctly, you’ll have to define your own print procedure for queues.” Explain what Eva Lu is talking about. In particular, show why Ben’s examples produce the printed results that they do. Define a procedure print-queue that takes a queue as input and prints the sequence of items in the queue.
(define (print-queue qs) (format #t "~a~%" (car qs)))
Instead of representing a queue as a pair of pointers, we can build a queue as a procedure with local state. The local state will consist of pointers to the beginning and the end of an ordinary list. Thus, the make-queue procedure will have the form
(define (make-queue) (let ((front-ptr … ) (rear-ptr … )) ⟨definitions of internal procedures⟩ (define (dispatch m) …) dispatch))
Complete the definition of make-queue and provide implementations of the queue operations using this representation.
(define (make-curryq) (let ((front-ptr '()) (rear-ptr '())) (define (set-fptr! item) (set! front-ptr item)) (define (set-rptr! item) (set! rear-ptr item)) (define (empty-curryq?) (null? front-ptr)) (define (front-curryq) (if (empty-curryq?) (error "FRONT on empty queue") (car front-ptr))) (define (insert-curryq! item) (let ((new-pair (cons item '()))) (cond [(empty-curryq?) (set-fptr! item) (set-rptr! item)] [else (set! rear-ptr new-pair) (set-rptr! new-pair)]))) (define (print-queue) (format #t "~a~%" front-ptr)) (define (dispatch m) (cond [(eq? m 'front-ptr) front-ptr] [(eq? m 'rear-ptr) rear-ptr] [(eq? m 'insert-queue!) insert-curryq!] [(eq? m 'print-queue) print-queue])) dispatch))
A deque (“double-ended queue”) is a sequence in which items can be inserted and deleted at either the front or the rear. Operations on deques are the constructor make-deque, the predicate empty-deque?, selectors front-deque and rear-deque, and mutators front-insert-deque!, rear-insert-deque!, front-delete-deque!, rear-delete-deque!. Show how to represent deques using pairs, and give implementations of the operations. All operations should be accomplished in Θ(1) steps.
This is the structure I’ve decided to use for the deque. There may be other neat ways to encode a deque with cons-cells. I’d love to hear if anyone has a better structure:
F: Front Ptr B: Back Ptr X: Value /: Null or End
-- --- ---—+ -- ---
|*||–>||*||*||–>||*||/|-|-—+ -|-—+ -|-—+
V ^—+ V ^—+ V
-- --- | ---—+ | ---—+
|X||/||X||*||X||*|---—+ | ---- - | ---- -
(define (make-deque) ’(() . ())) (define (empty-deque? dq) (null? (front-deque dq))) (define (front-deque dq) (car dq)) (define (rear-deque dq) (cdr dq)) (define (next-deque lst) (if (null? lst) ’() (cdr lst))) (define (prev-deque lst) (if (null? lst) ’() (cdar lst)))
(define (front-insert-deque! dq value) (let ([new-elt (cons (cons value ’()) ’())]) (cond ((empty-deque? dq) (set-car! dq new-elt) (set-cdr! dq new-elt) dq) (else ;; link our next element to the current front (set-cdr! new-elt (front-deque dq)) ;; find the next element to make a backwards link (set-cdr! (car (front-deque dq)) new-elt) (set-car! dq new-elt) dq))))
(define (rear-insert-deque! dq value) (let ([new-elt (cons (cons value ’()) ’())]) (cond ((empty-deque? dq) (set-car! dq new-elt) (set-cdr! dq new-elt) dq) (else ;; Link our backwards element (set-cdr! (car new-elt) (rear-deque dq)) (set-cdr! (rear-deque dq) new-elt) (set-cdr! dq new-elt) dq))))
(define (front-delete-deque! dq) (let ([next (next-deque (front-deque dq))] [front (front-deque dq)]) (cond ((null? next) (set-car! dq ’()) (set-cdr! dq ’())) (else (set-car! dq next) (set-cdr! (car (front-deque dq)) ’()))) front))
(define (rear-delete-deque! dq) (let ([rear (rear-deque dq)] [prev (prev-deque (rear-deque dq))]) (cond ((null? rear) (set-car! dq ’()) (set-cdr! dq ’())) (else (set-cdr! dq prev) (set-cdr! (rear-deque dq) ’()))) rear))
In the table implementations above, the keys are tested for equality using `equal?’ (called by `assoc’). This is not always the appropriate test. For instance, we might have a table with numeric keys in which we don’t need an exact match to the number we’re looking up, but only a number within some tolerance of it. Design a table constructor `make-table’ that takes as an argument a `same-key?’ procedure that will be used to test “equality” of keys. `Make-table’ should return a `dispatch’ procedure that can be used to access appropriate `lookup’ and `insert!’ procedures for a local table.
(define (make-table-with-key same-key?) (let ((local-table (list '*table*))) ;; just redefine `assoc' with `same-key?' (define (assoc key records) (cond ((null? records) #f) ((same-key? key (caar records)) (car records)) (else (assoc key (cdr records))))) (define (lookup key-1 key-2) (let ((subtable (assoc key-1 (cdr local-table)))) (if subtable (let ((record (assoc key-2 (cdr subtable)))) (if record (cdr record) #f)) #f))) (define (insert! key-1 key-2 value) (let ((subtable (assoc key-1 (cdr local-table)))) (if subtable (let ((record (assoc key-2 (cdr subtable)))) (if record (set-cdr! record value) (set-cdr! subtable (cons (cons key-2 value) (cdr subtable))))) (set-cdr! local-table (cons (list key-1 (cons key-2 value)) (cdr local-table)))))) (define (dispatch m) (cond ((eq? m 'lookup-proc) lookup) ((eq? m 'insert-proc!) insert!))) dispatch))
Generalizing one and two-dimensional tables, show how to implement a table in which values are stored under an arbitrary number of keys and different values may be stored under different numbers of keys. The `lookup’ and `insert!’ procedures should take as input a list of keys used to access the table.
The easiest way to accomplish this is to accept variadic arguments to `insert’ and `lookup’, folding them into a string or using the list directly (which `equal?’ can compare)
To search a table as implemented above, one needs to scan through the list of records. This is basically the unordered list representation of section *Note 2-3-3. For large tables, it may be more efficient to structure the table in a different manner. Describe a table implementation where the (key, value) records are organized using a binary tree, assuming that keys can be ordered in some way (e.g., numerically or alphabetically). (Compare Exercise 2-66 of Chapter 2)
The value that is to be inserted is converted into it’s numeric form. Insert & Lookup function as you would expect
“Memoization” (also called “tabulation”) is a technique that enables a procedure to record, in a local table, values that have previously been computed. This technique can make a vast difference in the performance of a program. A memoized procedure maintains a table in which values of previous calls are stored using as keys the arguments that produced the values. When the memoized procedure is asked to compute a value, it first checks the table to see if the value is already there and, if so, just returns that value. Otherwise, it computes the new value in the ordinary way and stores this in the table. As an example of memoization, recall from section 1-2-2 the exponential process for computing Fibonacci numbers:
(define (fib n) (cond ((= n 0) 0) ((= n 1) 1) (else (+ (fib (- n 1)) (fib (- n 2))))))
The memoized version of the same procedure is
(define memo-fib (memoize (lambda (n) (cond ((= n 0) 0) ((= n 1) 1) (else (+ (memo-fib (- n 1)) (memo-fib (- n 2))))))))
where the memoizer is defined as
(define (memoize f) (let ((table (make-table))) (lambda (x) (let ((previously-computed-result (lookup x table))) (or previously-computed-result (let ((result (f x))) (insert! x result table) result))))))
Draw an environment diagram to analyze the computation of `(memo-fib 3)’. Explain why `memo-fib’ computes the nth Fibonacci number in a number of steps proportional to n. Would the scheme still work if we had simply defined `memo-fib’ to be `(memoize fib)’?
memo-fib is O(N) because the fibonacci sequence can simply be computed in 2*(Σ(N)) steps (half of which are ’precomputed’) The scheme would not work if each function were freshly memoized because the `table’ would not be shared between the various applications of `memo-fib’.
Define an or-gate as a primitive function box. Your or-gate constructor should be similar to and-gate.
(define or-gate-delay 5) (define (or-gate a1 a2 output) (define (or-action-procedure) (let ((new-value (logior (signal-value a1) (signal-value a2)))) (after-delay or-gate-delay (λ () (set-signal! output new-value))))) (add-action! a1 or-action-procedure) (add-action! a2 or-action-procedure) 'ok)
Another way to construct an or-gate is as a compound digital logic device, built from and-gates and inverters. Define a procedure or-gate that accomplishes this. What is the delay time of the or-gate in terms of and-gate-delay and inverter-delay?
(!a1) && a2 is congruent to a1 || a2, it is as fast as (AND-DELAY + INVERTERDELAY)
TODO Exercise 3.30
Figure 3.27 shows a ripple-carry adder formed by stringing together n full-adders. This is the simplest form of parallel adder for adding two n -bit binary numbers. The inputs A 1 , A 2 , A 3 , …, A n and B 1 , B 2 , B 3 , …, B n are the two binary numbers to be added (each A k and B k is a 0 or a 1). The circuit generates S 1 , S 2 , S 3 , …, S n , the n bits of the sum, and C , the carry from the addition. Write a procedure ripple-carry-adder that generates this circuit. The procedure should take as arguments three lists of n wires each—the A k , the B k , and the S k —and also another wire C . The major drawback of the ripple-carry adder is the need to wait for the carry signals to propagate. What is the delay needed to obtain the complete output from an n -bit ripple-carry adder, expressed in terms of the delays for and-gates, or-gates, and inverters?
The internal procedure `accept-action-procedure!’ defined in make-wire specifies that when a new action procedure is added to a wire, the procedure is immediately run. Explain why this initialization is necessary. In particular, trace through the half-adder example in the paragraphs above and say how the system’s response would differ if we had defined accept-action-procedure! as
(define (accept-action-procedure! proc) (set! action-procedures (cons proc action-procedures)))
the signal value must be initialized or the entire system will run the action procedures (no matter what has changed)
TODO Exercise 3.32
The procedures to be run during each time segment of the agenda are kept in a queue. Thus, the procedures for each segment are called in the order in which they were added to the agenda (first in, first out). Explain why this order must be used. In particular, trace the behavior of an and-gate whose inputs change from 0, 1 to 1, 0 in the same segment and say how the behavior would differ if we stored a segment’s procedures in an ordinary list, adding and removing procedures only at the front (last in, first out).
Using primitive multiplier, adder, and constant constraints, define a procedure averager that takes three connectors a, b, and c as inputs and establishes the constraint that the value of c is the average of the values of a and b.
(define (averager a b c) (with-connectors (half sum) (constant 0.5 half) (adder a b sum) (multiplier half sum c) 'ok))
Louis Reasoner wants to build a squarer, a constraint device with two terminals such that the value of connector b on the second terminal will always be the square of the value a on the first terminal. He proposes the following simple device made from a multiplier:
(define (squarer a b) (multiplier a a b))
There is a serious flaw in this idea. Explain.
The value of `a’ is not “duplicated” across – so `process-new-value’ only reads either `rhs’ or `lhs’’s value
Ben Bitdiddle tells Louis that one way to avoid the trouble in Exercise 3.34 is to define a squarer as a new primitive constraint. Fill in the missing portions in Ben’s outline for a procedure to implement such a constraint:
(define (squarer a b) (define (process-new-value) (if (has-value? b) (if (< (get-value b) 0) (error “square less than 0: SQUARER” (get-value b)) ⟨alternative1⟩) ⟨alternative2⟩)) (define (process-forget-value) ⟨body1⟩) (define (me request) ⟨body2⟩) ⟨rest of definition⟩ me)
(define-class <squarer> (<constraint>) ;; in squarer, there is essentially only one value (rhs #:allocation #:virtual #:slot-ref (lambda (o) (slot-ref o 'lhs)) #:slot-set! (lambda (o s) (slot-set! o 'lhs s))) ;; strictly speaking these are not nessasary, as they are defined directly in ;; `process-new-value', they are kept for posteriety. (operator #:init-value square) (inverse-operator #:init-value sqrt)) (define-method (process-new-value (c <squarer>)) (let* ([lhs-conn (lhs c)] [total-conn (total c)] [has-total? (has-value? total-conn)] [has-lhs? (has-value? lhs-conn)]) ;; Determine what values *are* known and set the appropriate connector. (cond ;; (lhs < 0) => error [(and (has-lhs? (< (connector-value lhs-conn) 0))) (error "square less than 0: SQUARER" (connector-value lhs-conn))] ;; lhs = √total [has-total? (set-value! lhs-conn (sqrt (connector-value total-conn)) c)] ;; total = lh² [has-lhs? (set-value! total-conn (square (connector-value lhs-conn)) c)]))) (define-method (process-forget-value (c <squarer>)) (forget-value! (lhs c) c) (forget-value! (total c) c) (process-new-value c))
TODO Exercise 3.36
Suppose we evaluate the following sequence of expressions in the global environment:
(define a (make-connector)) (define b (make-connector)) (set-value! a 10 ’user)
At some time during evaluation of the set-value!, the following expression from the connector’s local procedure is evaluated:
(for-each-except setter inform-about-value constraints)
Draw an environment diagram showing the environment in which the above expression is evaluated.
The celsius-fahrenheit-converter procedure is cumbersome when compared with a more expression-oriented style of definition, such as
(define (celsius-fahrenheit-converter x) (c+ (c* (c/ (cv 9) (cv 5)) x)
(define C (make-connector)) (define F (celsius-fahrenheit-converter C))
Here c+, c*, etc. are the “constraint” versions of the arithmetic operations. For example, c+ takes two connectors as arguments and returns a connector that is related to these by an adder constraint:
(define (c+ x y) (let ((z (make-connector))) (adder x y z) z))
Define analogous procedures c-, c*, c/, and cv (constant value) that enable us to define compound constraints as in the converter example above.
(define (c+ augend addend) (with-connectors (sum) (adder augend addend sum) sum)) (define (c- minuend subtrahend) (with-connectors (difference) (adder difference subtrahend minuend) difference)) (define (c* multiplicand m2) (with-connectors (product) (multiplier multiplicand m2 product) product)) (define (c/ dividend divisor) (with-connectors (quotient) (multiplier quotient divisor dividend) quotient)) (define (cv value) (with-connectors (result) (constant value result) result))
Suppose that Peter, Paul, and Mary share a joint bank account that initially contains $100. Concurrently, Peter deposits $10, Paul withdraws $20, and Mary withdraws half the money in the account, by executing the following commands:
Peter: (set! balance (+ balance 10)) Paul: (set! balance (- balance 20)) Mary: (set! balance (- balance (/ balance 2)))
- List all the different possible values for balance after these three transactions have been completed, assuming that the banking system forces the three processes to run sequentially in some order.
- What are some other values that could be produced if the system allows the processes to be interleaved? Draw timing diagrams like the one in Figure 3.29 to explain how these values can occur.
Peter->Paul->Mary: (calc-eval “((100+10)-20)/2”) => 45 Peter->Mary->Paul: (calc-eval “((100+10)/2)-20”) => 35 Mary->Paul->Peter: (calc-eval “((100 / 2) - 20) + 20”) => 50 Mary->Peter->Paul: (calc-eval “((100 / 2) + 10) - 20”) => 40 Paul->Peter->Mary: (calc-eval “((100 - 20) + 10) / 2”) => 45 Paul->Mary->Peter: (calc-eval “((100 - 20) / 2) + 10”) => 50
Which of the five possibilities in the parallel execution shown above remain if we instead serialize execution as follows:
(define x 10) (define s (make-serializer)) (parallel-execute (lambda () (set! x ((s (lambda () (* x x)))))) (s (lambda () (set! x (+ x 1)))))
101, 100, 121
Give all possible values of x that can result from executing
(define x 10) (parallel-execute (lambda () (set! x (* x x))) (lambda () (set! x (* x x x))))
Which of these possibilities remain if we instead use serialized procedures:
(define x 10) (define s (make-serializer)) (parallel-execute (s (lambda () (set! x (* x x)))) (s (lambda () (set! x (* x x x)))))
x*6 (multiplication is commutative)
Ben Bitdiddle worries that it would be better to implement the bank account as follows (where the commented line has been changed):
(define (make-account balance) (define (withdraw amount) (if (>= balance amount) (begin (set! balance (- balance amount)) balance) “Insufficient funds”)) (define (deposit amount) (set! balance (+ balance amount)) balance) (let ((protected (make-serializer))) (define (dispatch m) (cond ((eq? m ’withdraw) (protected withdraw)) ((eq? m ’deposit) (protected deposit)) ((eq? m ’balance) ((protected (lambda () balance)))) ; serialized (else (error “Unknown request: MAKE-ACCOUNT” m)))) dispatch))
because allowing unserialized access to the bank balance can result in anomalous behavior. Do you agree? Is there any scenario that demonstrates Ben’s concern?
First off, this question is phrased in a really shitty way. Second off, no BALANCE is safe. the other functions are UNSAFE
Ben Bitdiddle suggests that it’s a waste of time to create a new serialized procedure in response to every withdraw and deposit message. He says that make-account could be changed so that the calls to protected are done outside the dispatch procedure. That is, an account would return the same serialized procedure (which was created at the same time as the account) each time it is asked for a withdrawal procedure.
(define (make-account balance) (define (withdraw amount) (if (>= balance amount) (begin (set! balance (- balance amount)) balance) “Insufficient funds”)) (define (deposit amount) (set! balance (+ balance amount)) balance) (let ((protected (make-serializer))) (let ((protected-withdraw (protected withdraw)) (protected-deposit (protected deposit))) (define (dispatch m) (cond ((eq? m ’withdraw) protected-withdraw) ((eq? m ’deposit) protected-deposit) ((eq? m ’balance) balance) (else (error “Unknown request: MAKE-ACCOUNT” m)))) dispatch)))
This is fine
TODO Exercise 3.43
Suppose that the balances in three accounts start out as $10, $20, and $30, and that multiple processes run, exchanging the balances in the accounts. Argue that if the processes are run sequentially, after any number of concurrent exchanges, the account balances should be $10, $20, and $30 in some order. Draw a timing diagram like the one in *Note Figure 3-29:: to show how this condition can be violated if the exchanges are implemented using the first version of the account-exchange program in this section. On the other hand, argue that even with this `exchange’ program, the sum of the balances in the accounts will be preserved. Draw a timing diagram to show how even this condition would be violated if we did not serialize the transactions on individual accounts.
SKIPPED: Timing diagram
TODO Exercise 3.47
A semaphore (of size n ) is a generalization of a mutex. Like a mutex, a semaphore supports acquire and release operations, but it is more general in that up to n processes can acquire it concurrently. Additional processes that attempt to acquire the semaphore must wait for release operations. Give implementations of semaphores
- In terms of mutexes
- In terms of atomic test-and-set! operations.