CS 61A Week 1 Lab and Homework Solutions FIRST LAB: No problems that required original solutions! SECOND LAB: 1. Nothing original. 2. If the last letter is Y, then we have to look at the next-to-last: (define (plural wd) (if (and (equal? (last wd) 'y) (not (vowel? (last (bl wd))))) (word (bl wd) 'ies) (word wd 's))) If you didn't think to use AND in that way, it can be done with nested IFs: (define (plural wd) (if (equal? (last wd) 'y) (if (vowel? (last (bl wd))) (word wd 's) (word (bl wd) 'ies)) (word wd 's))) Or, if that's too messy, with a subprocedure: (define (plural wd) (if (equal? (last wd) 'y) (y-plural (bl wd)) (word wd 's))) (define (y-plural prefix) (if (vowel? (last prefix)) (word prefix 'ys) (word prefix 'ies))) All of these assume the definition of vowel? in the handout. 3. There are, of course, many possible ways to write this. None is perfectly elegant. The difficulty is figuring out which of the three arguments is smallest, so you can leave it out of the computation. The way I like best, I think, is a little tricky: (define (sum-square-large papa mama baby) (define (square x) (* x x)) (cond ((> mama papa) (sum-square-large mama papa baby)) ((> baby mama) (sum-square-large papa baby mama)) (else (+ (square papa) (square mama))))) I think this way is pretty concise and easy to read. However, it's okay if you did it more straightforwardly like this: (define (sum-square-large a b c) (define (square x) (* x x)) (define (sumsq x y) (+ (square x) (square y))) (cond ((and (<= a b) (<= a c)) (sumsq b c)) ((and (<= b a) (<= b c)) (sumsq a c)) (else (sumsq a b)) )) If you didn't think of using AND to identify the conditions, it could also be done using nested IFs: (define (sum-square-large a b c) (define (square x) (* x x)) (define (sumsq x y) (+ (square x) (square y))) (if (>= a b) (if (>= b c) (sumsq a b) (sumsq a c)) (if (>= a c) (sumsq a b) (sumsq b c)))) Some people wanted to start by solving a subproblem: a function to find the two largest numbers. This can be done, but it's harder: (define (sum-square-large a b c) (define (square x) (* x x)) (define (sumsq nums) (+ (square (first nums)) (square (last nums)))) (define (two-largest a b c) (cond ((and (<= a b) (<= a c)) (sentence b c)) ((and (<= b a) (<= b c)) (sentence a c)) (else (sentence a b)))) (sumsq (two-largest a b c))) The trick here is that a function can't return two values, so two-largest has to return a sentence containing the two numbers. This hardly seems worth the effort, but the attempt to split the problem into logical pieces was well-motivated. It's a good idea in general, but it didn't work out well this time. See also: http://code.google.com/p/jrm-code-project/wiki/ProgrammingArt 4. Since we are examining each word of a sentence, the solution will involve a recursion of the form (dupls-removed (bf sent)). The base case is an empty sentence; otherwise, there are two possibilities, namely, (first sent) either is or isn't duplicated later in the sentence. (define (dupls-removed sent) (cond ((empty? sent) '()) ((member? (first sent) (bf sent)) (dupls-removed (bf sent))) (else (sentence (first sent) (dupls-removed (bf sent)))))) ============================================================ HOMEWORK: 1. The Scheme interpreter applies an ordinary procedure by first evaluating all the argument expressions and then invoking the procedure. Consider first one of the examples that worked: > (new-if (= 2 3) 0 5) Scheme evaluates this expression as follows: (a) Evaluate the symbol new-if. Its value turns out to be an ordinary procedure. Therefore the rest of the combination is evaluated normally. (b) Evaluate the three argument expressions. Their values are #f [i.e., false], 0, and 5 respectively. (c) Apply the procedure new-if to the argument values #f, 0, and 5. By the substitution model, this means we must substitute "#f" for "predicate", "0" for "then-clause", and "5" for "else-clause": (cond (#f 0) (else 5)) (d) Evaluate this new expression, getting the value 5. By contrast, if we'd entered the expression > (if (= 2 3) 0 5) Scheme would evaluate it as follows: (a) Notice that the symbol IF is a keyword, the name of a special form. Therefore the rest of the combination is evaluated specially. (b) Invoke the special form with the UNEVALUATED argument expressions "(= 2 3)", "0", and "5". (c) The "if" evaluation rule then causes its first argument to be evaluated. Since the value is #f, i.e. false, it then evaluates the expression "5", whose value is the number 5. The expression "0" is never evaluated. In the example above, it doesn't make any PRACTICAL difference that the expression "5" was evaluated to produce the number 5. [By the way, Scheme uses quotation marks for a special purpose, which isn't what I mean here. I'm just using them to delimit something you're to imagine as having typed into the computer.] Now, on to the square root program. In the body of sqrt-iter, the third and final argument to new-if is the expression (sqrt-iter (improve guess x) x) Suppose we invoke sqrt-iter with an expression like (sqrt-iter 1 4) Since sqrt-iter and new-if are both ordinary procedures, they are applied just like the new-if example I described earlier: (a) Evaluate the symbol sqrt-iter. Its value turns out to be an ordinary procedure. Therefore the rest of the combination is evaluated normally. (b) Evaluate the two argument expressions. Their values are 1 and 4, respectively. (c) Apply the procedure sqrt-iter to the argument values 1 and 4. By the substitution model, this means we must substitute "1" for "guess" and "4" for "x": (new-if (good-enough? 1 4) 1 (sqrt-iter (improve 1 4) 4)) (d) Evaluate this new expression. Here is where the problem comes in. Since new-if is an ordinary procedure, we follow steps (a)-(d) for this sub-evaluation also: (a) Evaluate the symbol new-if. Its value turns out to be an ordinary procedure. Therefore the rest of the combination is evaluated normally. (b) Evaluate the three argument expressions. The first one turns out (after a sequence of (a)-(d) steps) to have the value #f, i.e., false. The second has the value 1. The third invokes sqrt-iter, so we start another (a)-(d) sequence of steps just like the first one. But the first one is still pending, waiting for us to finish down here. That is, the evaluation of the original (sqrt-iter 1 4) is waiting for the evaluation of the new-if expression, and that's waiting for the evaluation of the new sqrt-iter expression. But THAT will involve evaluating another new-if expression, which will... This is an infinite regress. You'll never get any answer at all. This business of nested evaluations isn't all wrong. In the real sqrt-iter the same thing happens, with sqrt-iter invoking if, and if invoking sqrt-iter, and so on. The difference is that with the real if, a special form, Scheme gets to test whether the good-enough? expression is true or false BEFORE it evaluates the inner sqrt-iter expression. At first the good-enough? expression is false, so if invokes sqrt-iter repeatedly just as in the new-if version. But eventually good-enough? returns a true [that is, #t] value, and then the inner sqrt-iter expression need not be evaluated. With new-if, we needed to evaluate the inner sqrt-iter before we had a chance to see if good-enough? came out true or false. Therefore Scheme never finds out that it's time to stop iterating. 2. (define (squares nums) (if (empty? nums) '() (se (square (first nums)) (squares (bf nums)) ))) 3. The tricky part is that the first word of the sentence must be treated specially, so there has to be a top-level procedure that handles it and also invokes a recursive subprocedure for the rest of the words: (define (switch sent) (se (switch-first (first sent)) (switch-rest (bf sent)) )) (define (switch-first wd) (cond ((equal? wd 'you) 'I) ((equal? wd 'I) 'you) ((equal? wd 'me) 'you) (else wd) )) (define (switch-rest sent) (if (empty? sent) '() (se (switch-one (first sent)) (switch-rest (bf sent)) ))) (define (switch-one wd) (cond ((equal? wd 'you) 'me) ((equal? wd 'I) 'you) ((equal? wd 'me) 'you) (else wd) )) 4. (define (ordered? sent) (cond ((empty? (bf sent)) #t) ((> (first sent) (first (bf sent))) #f) (else (ordered? (bf sent))) )) This procedure is written assuming that the argument is a sentence that contains at least one word, and that all of the words are numbers. 5. (define (ends-e sent) (cond ((empty? sent) '()) ((equal? (last (first sent)) 'e) (se (first sent) (ends-e (bf sent)))) (else (ends-e (bf sent))))) 6. Are "and" and "or" ordinary functions or special forms? The general idea of the solution is to type in an expression that will produce an error if all its subexpressions are evaluated, and see if they all are. For example, supposing there is no definition for the symbol "x" you could say > (or 1 2 x) According to the ordinary evaluation rule, the expressions "1" "2" and "x" should all be evaluated before "or" is invoked, so you should get an error message complaining about the unbound symbol. On the other hand, if "or" is a special form, you'd expect it to stop as soon as it evaluates the "1" and give 1 as its result. If you try this in Scheme, you don't get an error message. This means, most likely, that "or" is a special form whose arguments are evaluated one by one. If there were an error message could you conclude that "or" is ordinary? No! "Or" could be a special form that evaluates its arguments right-to-left. For that matter there is no reason that "or" couldn't evaluate the middle argument first. How would you test for that? (Of course, in reality you know that they're special forms because the textbook told you so.) Why might a special form be a good idea? Here are a few reasons: (a) Efficiency. Suppose instead of numbers or symbols I used combinations as the arguments to "or", and each combination takes several minutes to compute. If the first one happens to be true, it'd be a shame to waste all that time computing the others. (b) Conditions that depend on each other. Consider the expression > (or (= x 0) (> 5 (/ y x))) This will work fine if "or" is special and evaluates left-to-right, otherwise we may be dividing by zero. Reasons why an ordinary function might be preferred: (c) Fewer kludges. It's very easy to read and understand a Lisp program if you can be sure that everything that looks like (blah glorp zilch) is evaluated by evaluating the subexpressions and then applying the procedure "blah" to the arguments "glorp" and "zilch". Everything that looks like a procedure application but is really a special case just makes things that much harder to understand. (d) Creeping featurism. Where do we stop? Maybe we should make multiplication a special form; after all, in the expression > (* 0 x) there's no real reason to evaluate x because we know zero times anything is zero. Pretty soon there are no real functions left in the language. (e) Functional objects. You're not expected to know this yet, but next week you'll learn that procedures can be manipulated as data, just as numbers can. But special forms aren't procedures and there are some things we can't do to them.