MASSACHVSETTS INSTITVTE OF TECHNOLOGY Department of Electrical Engineering and Computer Science 6.945 Spring 2009 Problem Set 3 Issued: Wed. 18 Feb. 2009 Due: Wed. 25 Feb. 2009 Reading: SICP, From Chapter 4: 4.1 and 4.2; (pp. 359--411) en.wikipedia.org/wiki/Evaluation_strategy www.cs.indiana.edu/cgi-bin/techreports/TRNNN.cgi?trnum=TR44 Code: load.scm, utils.scm, ghelper.scm, syntax.scm, rtdata.scm, interp.scm, repl.scm general-procedures.scm code is on the class web page... no reason to kill more trees. Evaluators for Extended Scheme You will be working with an evaluator system for an extended version of Scheme similar to the ones described in SICP, Chapter 4. Without a good understanding of how the evaluator is structured it is very easy to become confused between the programs that the evaluator is interpreting, the procedures that implement the evaluator itself, and Scheme procedures called by the evaluator. You will need to study Chapter 4 through subsection 4.2.2 carefully in order to do this assignment. The interpreters in the code that we will work with in this problem set are built on the generic operations infrastructure we developed in the last problem set. (Actually, there is a small change: we specify a handler with "defhandler" as an alias for "assign-operation". Also, we allow handlers to be specified without declaring all of the arguments, avoiding the need for "any?".) Indeed, in these interpreters, unlike the ones in SICP, EVAL and APPLY are generic operations! That means that we may easily extend the types of expressons (by adding new handlers to EVAL) and the types of procedures (by adding new handlers to APPLY). Before beginning work on this problem set you should carefully read the code in interp.scm. Also, look at the difference between the ghelper.scm in this problem set and the ghelper.scm in the previous set. Using the generic interpreter Download the supplied code to a fresh directory in your computer. Get a fresh Scheme system, and load the file load.scm: (load "/load") Initialize the evaluator: (init) You will get a prompt: eval> You can enter an expression at the prompt: eval> (define cube (lambda (x) (* x x x))) cube eval> (cube 3) 27 The evaluator code we supplied does not have an error system of its own, so it reverts to the underlying Scheme error system. (Maybe an interesting little project? It is worth understanding how to make exception-handling systems!) If you get an error, clear the error with two control Cs and then continue the evaluator with "(go)" at the Scheme. If you redo "(init)" you will lose the definition of cube, because a new environment will be made. eval> (cube a) ;Unbound variable a ;Quit! (go) eval> (cube 4) 64 eval> (define (fib n) (if (< n 2) n (+ (fib (- n 1)) (fib (- n 2))))) fib eval> (fib 10) 55 You can always get out of the generic evaluator and get back to the underlying Scheme system by quitting (with two control Cs). ------------- Problem 3.1: Warmup In mathematical text a common abuse of notation is to identify a tuple of functions with a function that returns a tuple of values. For example, (written in Lisp prefix form) If (cos 0.6) ==> 0.8253356149096783 and (sin 0.6) ==> 0.5646424733950354 then we expect ((vector cos sin) 0.6) ==> #(0.8253356149096783 0.5646424733950354) This requires that an extension to APPLY so it can handle Scheme vectors as a kind of function. Make this extension; demonstrate it; show that it interoperates with more conventional code. ------------- ------------- Problem 3.2: Unbound-variable handling In Lisps, including Scheme, attempting to evaluate an unbound symbol is an unbound-variable error. However, in some algebraic processes it is sensible to allow an unbound symbol to be a self-evaluating object. For example, if we generically extend arithmetic to build algebraic expressions with symbolic values, it is sometimes useful to allow the following: (+ (* 2 3) (* 4 5)) ==> 26 (+ (* a 3) (* 4 5)) ==> (+ (* a 3) 20) In this case, the symbol "a" is unbound and self-evaluating. The operators "*" and "+" are extended to just build expressions when their arguments are not reducible to numbers. Make generic extensions to +, *, -, /, and to EVAL, to allow this kind of behavior. Also augment APPLY to allow literal functions, known only by their names: (+ (f 3) (* 4 5)) ==> (+ (f 3) 20) These extensions to EVAL and APPLY are generally dangerous, because they hide real unbound-variable errors. Make them contingent on the value of a user-settable variable: ALLOW-SELF-EVALUATING-SYMBOLS. ------------- Much more powerful extensions are available once we accept generic operations at this level. For example, we can allow procedures to have both strict and non-strict arguments. If you don't know what we are talking about here please read the article: http://en.wikipedia.org/wiki/Evaluation_strategy. If you load the file general-procedures.scm into the underlying Scheme, after loading (with '(load "load")') the generic interpreter, you will find that there are extensions that allow us to define procedures with some formal parameters asking for the matching arguments to be lazy (or lazy and memoized). Other undecorated parameters take their arguments strictly. These extensions make it relatively easy to play otherwise painful games. For example, we may define the UNLESS conditional as an ordinary procedure: ;Quit! (load "general-procedures" generic-evaluation-environment) ;Loading "general-procedures.scm"... ;Warning: Replacing top-level handler ;... done ;Value: #[compound-procedure 17 operator] (go) eval> (define unless (lambda (condition (usual lazy) (exception lazy)) (if condition exception usual))) We may use the usual define abbreviations (see syntax.scm): eval> (define (unless condition (usual lazy) (exception lazy)) (if condition exception usual)) unless eval> (define (ffib n) (unless (< n 2) (+ (ffib (- n 1)) (ffib (- n 2))) n)) ffib eval> (ffib 10) 55 Notice that UNLESS is declared to be strict in its first argument (the predicate) but nonstrict in the alternatives: neither alternative will be evaluated until it is necessary. Additionally, if we include the file kons.scm we get a special form that is the non-strict memoized version of CONS. This immediately gives us the power of infinite streams: ;Quit! (load "kons" generic-evaluation-environment) ;Loading "kons.scm"... done ;Value: #[compound-procedure 19 operator] (go) eval> (define (add-streams s1 s2) (kons (+ (car s1) (car s2)) (add-streams (cdr s1) (cdr s2)))) add-streams eval> (define (ref-stream stream n) (if (= n 0) (car stream) (ref-stream (cdr stream) (- n 1)))) ref-stream eval> (define fibs (kons 0 (kons 1 (add-streams (cdr fibs) fibs)))) fibs eval> (ref-stream fibs 10) 55 eval> (ref-stream fibs 20) 6765 eval> (ref-stream fibs 30) 832040 eval> (ref-stream fibs 40) 102334155 ------------- Problem 3.3: Streams a. The non-strict procedure KONS adds great power to the system. Notice that there is no need to make CAR or CDR different to obtain the use of streams. In a short paragraph explain why KONS is almost sufficient. It may be instructive to read an ancient paper by Friedman and Wise: www.cs.indiana.edu/cgi-bin/techreports/TRNNN.cgi?trnum=TR44 b. Unfortunately, the addition of KONS does not, in itself, solve all stream problems. For example, the difficulty alluded to in SICP section 4.2.3 (p. 411) does not automatically dissipate. If we make the following definitions: (define (map-stream proc items) (kons (proc (car items)) (map-stream proc (cdr items)))) (define (scale-stream items factor) (map-stream (lambda (x) (* x factor)) items)) (define (integral integrand initial-value dt) (define int (kons initial-value (add-streams (scale-stream integrand dt) int))) int) (define (solve f y0 dt) (define y (integral dy y0 dt)) (define dy (map-stream f y)) y) and then we try: (ref-stream (solve (lambda (x) x) 1 0.001) 1000) we will get an error (try it!). Why? Explain the error. What other declarations should be made in these stream-procedure definitions to fix all such errors? ------------- ------------- Problem 3.4: Why not? a. The KONS special form is equivalent to a CONS with both arguments lazy and memoized. If the arguments were not memoized the computation (ref-stream fibs 40) in Problem 3.3a above would take a very long time. Is there ever any advantage to not memoizing? When might it matter? b. Why, given that CONS is a strict procedure imported from Scheme, could we not have defined KONS simply as: (define (kons (a lazy memo) (d lazy memo)) (cons a d)) ? c. More generally, the Lisp community has avoided changing CONS to be KONS, as recommended by Friedman and Wise. What potentially serious problems are avoided by using CONS rather than KONS? Assume that we do not care about small constant factors in performance. ------------- ------------- Problem 3.5: Your turn Invent some fun, interesting construct that can easily be implemented using generic EVAL/APPLY that would be rather painful without that kind of generic support. Show us the construct, the implementation, and some illustrative examples. Enjoy! -------------