----------------------------------------- Tutorial notes #7 for 4/2 or 4/3/2007 TA: Stephen McCamant Email: smcc@mit.edu Location: 36-113A or 36-115 http://people.csail.mit.edu/smcc/6.001-sp07/ ----------------------------------------- Project 2 hand-back Median grade (my section): 94/100 Median guessing game score: 52.5/100 ----------------------------------------- Project 2 common issues - Don't re-implement standard functions: even?, expt, foldr, reverse - Use the question sequence as a hint about what procedures to use, e.g.: fand, negation in true-but-not-always - Higher-order functions can be more elegant if you can avoid the need for an extra lambda. Examples: negation, skip-most-recent-n - guess-last is better than car - Watch out for ambiguities in the procedure specifications (questions 5 and 7 were both bad this way) - For writing questions, be sure to answer *why*, not just what - Don't forget to test - Testing isn't just about running you code. You also have to be able tell whether the results were right. - Question 12 was an example of code that is hard to test, since it's random and returns only #t or #f Common mistake: returning a function (supplied code was not type-defensive) Ideas on good ways to test? 1/2 + 1/8 + 1/32 + 1/128 + ... = ? - If the learning game beat you by more than a few points, you weren't trying hard enough (but I didn't deduct points) - Question 18 was open-ended, so to get maximum credit, we were looking for an answer that was both original and good. Not very original: combinations of existing functions, (lastn infinity) Common bad idea: randomized functions If the player is playing 90% #t and 10% #f, what's the best strategy to have in allfuncs? - In question 19, using irrational numbers wasn't actually a very helpful suggestion, since DrScheme's fractions only have about 50 bits of information. Getting around too-few digits Don't have to be truly random, just weird enough to fool the learner - Redundancies I got tired of seeing: (if pred X X) (if pred #t #f) (if pred #f #t) (eq? X #t) (eqv? X #t) (equal? X #t) (eq? X #f) ((lambda (n) n) X) ((lambda (a) (X a)) v) ----------------------------------------- Project 3 suggestions - We're asking you to write bigger pieces of code now, so plan ahead about how to break things up into procedures. - Useful procedure: for-each is like map, but doesn't make a list of the results. - Watch out: some of the supplied accessors are not defensive about checking tags. - There are a number of places where fold-right is useful, if you're comfortable with it. ----------------------------------------- Mutation review (set! variable value) (set-car! pair value) (set-cdr! pair value) set! is a special form that mutates variable bindings. set-car! and set-cdr! are regular procedures that change the parts of pairs. ----------------------------------------- Different kinds of mutation The procedure sqr!-and-count-odd takes a list of integers, mutates it by squaring each element, and returns the number of elements that were odd. For instance: (define lst (list 2 7 1 8 2 8)) (sqr!-and-count-odd lst) -> 2 lst -> (4 49 1 64 4 64) Fill in the blanks in the implementation: (define (sqr!-and-count-odd lst) (let ((counter _______)) (define (helper lst) (if (________ (null? lst)) (______ (_______ lst (sqr (car lst))) (if (odd? (car lst)) (______ _______ _______) (helper _______)))) ________ _______)) (define (sqr!-and-count-odd lst) (let ((counter 0)) (define (helper lst) (if (not (null? lst)) (begin (set-car! lst (sqr (car lst))) (if (odd? (car lst)) (set! counter (+ counter 1))) (helper (cdr lst))))) (helper lst) counter)) ----------------------------------------- filter! Let's write filter!, which is like filter except it mutates its argument list. Because the filtered list might start from a different pair than the original one, you need to use set! when getting the results: (define lst (list 3 1 4 1 5 9 2 6 5 3 5)) (set! lst (filter! even? lst)) lst -> (4 2 6) The easiest version of filter! to write is a recursive one based on the recursive non-mutation implementation we've see before: (define (filter f lst) (cond ((null? lst) '()) ((f (car lst)) (cons (car lst) (filter f (cdr lst)))) (else (filter f (cdr lst))))) What needs to be changed to make it mutate? ;; Just change cons to set-cdr!: (define (filter! f lst) (cond ((null? lst) '()) ((f (car lst)) (set-cdr! lst (filter! f (cdr lst)))) (else (filter! f (cdr lst))))) Writing an iterative version is trickier, though. We'd like to use the following pattern, but what do we put in the blank? (define (filter! f lst) (define (helper l) (cond ((null? l)) ((f (car l)) (helper (cdr l))) (else _________________________ (helper (cdr l))))) (helper lst) lst) There's really no way to get it to work just by filling in that blank, which is why it's hard. There are two problems: first, to remove an element, you need to change the cdr pointer of the previous element in the list, but there's no easy way to get back to it. Second, you can't just return "lst", since that always points at the first pair of the original list, which you may not want to include in the result. What should you do instead? I can think of at least three approaches. Approach 1 is the closest to the fill-in-the-blank version above, but more extensive changes are needed. Solving each of the problems mentioned above requires an additional variable, which you need to use and update correctly. (define (filter!-1 pred lst) (let ((last '()) (head '())) (define (helper lst) (cond ((null? lst)) ((pred (car lst)) (if (not (null? last)) (set-cdr! last lst)) (if (null? head) (set! head lst)) (set! last lst) (helper (cdr lst))) (else (helper (cdr lst))))) (helper lst) (if (not (null? last)) (set-cdr! last '())) head)) Approach 2 takes advantage of the fact that the natural way of iteratively going through a list reverses it. The iterative helper "iter" is like reverse!, except that it removes pairs whose cars don't satisfy the predicate. This gives a list with the right elements, but backwards, so you need to call reverse! on it at the end. (define (filter!-2 f lst) (define (iter from to) (if (null? from) to (let ((next (cdr from))) (cond ((f (car from)) (set-cdr! from to) (iter next from)) (else (iter next to)))))) (reverse! (iter lst '()))) Approach 3 is back to something more like approach 1, but more elegant thanks to a clever trick. To avoid the special cases related to "last" and "head" from approach 1, we can put a placeholder pair at the front of the list. It provides an initial value for "last" it will always be safe to modify, and its cdr plays the role of "head" remembering the pointer to return. (define (filter!-3 pred lst) (let ((handle (cons '() lst))) (define (helper last curr) (cond ((null? curr)) ((pred (car curr)) (helper curr (cdr curr))) (else (set-cdr! last (cdr curr)) (helper last (cdr curr))))) (helper handle lst) (cdr handle))) ----------------------------------------- Looking for loops Using mutation, we can create data structures that look like lists with infinitely repeating sections: (define four-thirds (list 'one 'point 'three)) (set-cdr! (cddr four-thirds) (cddr four-thirds)) (list-head four-thirds 5) -> (one point three three three) (list-head four-thirds 10) -> (one point three three three three three three three three) Write a procedure has-loop? to check whether its argument is a list with a cdr-loop, like the above, or a regular list. Here's a brute-force but fairly simple approach: we'll know we have a loop if the i-th and j-th pairs of the list counting from the front are the same, but i < j. So we can just try all such pairs of integers, being careful cover every pair and stop if we ever find an end. (define (has-loop-in-first-k? j lst) (define (loop i) (cond ((= i j) #f) ((eq? (list-tail lst i) (list-tail lst j)) #t) (else (loop (+ i 1))))) (loop 0)) (define (has-loop? lst) (define (iter k) (cond ((null? (list-tail lst k)) #f) ((has-loop-in-first-k? k lst) #t) (else (iter (+ k 1))))) (iter 0)) There's a actually a cleverer way of doing this that uses just pointers, and not counting, invented by Robert Floyd in 1967.