2008-09-19 Introduction to Scheme continued ======================================================================== Example for recursive function involving lists: (define (list-length list) (if (null? list) 0 (+ 1 (list-length (cdr list))))) Use different tools, esp: * syntax-checker * stepper How come we could use `list' as an argument -- use the syntax checker (define (list-length-helper list len) (if (null? list) len (list-length-helper (cdr list) (+ len 1)))) (define (list-length list) (list-length-helper list 0)) Main idea: lists are a recursive structure, so functions that operate on lists should be recursive functions that follow the recursive definition of lists. Another example for list function -- summing a list of numbers (define (sum-list l) (if (null? l) 0 (+ (car l) (sum-list (cdr l))))) Also show how to implement `rcons', using this guideline. ======================================================================== More examples: Define `reverse' -- solve the problem using `rcons'. `rcons' can be generalized into something very useful: `append'. * How would we use `append' instead of `rcons'? * How much time will this take? Does it matter if we use `append' or `rcons'? Redefine `reverse' using tail recursion. * Is the result more complex? (Yes, but not too bad because it collects the elements in reverse.) ======================================================================== When you have some common value that you need to use in several places, it is bad to duplicate it. For example: (define (how-many a b c) (cond [(> (* b b) (* 4 a c)) 2] [(= (* b b) (* 4 a c)) 1] [(< (* b b) (* 4 a c)) 0])) What's bad about it? * It's longer than necessary, which will eventually make your code less readable. * It's slower -- by the time you reach the last case, you have evaluated the two sequences three times. * It's more prone to bugs -- the above code is short enough, but what if it was longer so you don't see the three occurrences on the same page? Will you remember to fix all places when you debug the code months after it was written? In general, the ability to use names is probably the most fundamental concept in computer science -- the fact that makes computer programs what they are. We already have a facility to name values: function arguments. We could split the above function into two like this: (define (how-many-helper b^2 4ac) ; note the identifier name! (cond [(> b^2 4ac) 2] [(= b^2 4ac) 1] [(< b^2 4ac) 0])) (define (how-many a b c) (how-many-helper (* b b) (* 4 a c))) But instead of the awkward solution of coming up with a new function just for its names, we have a facility to bind local names -- `let'. In general, the syntax for a `let' special form is (let ([id expr] ...) expr) For example, (let ([x 1] [y 2]) (+ x y)) But note that the bindings are done "in parallel", for example, try this: (let ([x 1] [y 2]) (let ([x y] [y x]) (list x y))) Using this for the above problem: (define (how-many a b c) (let ([b^2 (* b b)] [4ac (* 4 a c)]) (cond [(> b^2 4ac) 2] [(= b^2 4ac) 1] [(< b^2 4ac) 0]))) ======================================================================== Some notes on writing code (also see the style-guide in the handouts section) *** Code quality will be graded to in this course! * Use abstractions whenever possible, as said above. This is bad: (define (how-many a b c) (cond ((> (* b b) (* 4 a c)) 2) ((= (* b b) (* 4 a c)) 1) ((< (* b b) (* 4 a c)) 0))) (define (what-kind a b c) (cond ((= a 0) 'degenerate) ((> (* b b) (* 4 a c)) 'two) ((= (* b b) (* 4 a c)) 'one) ((< (* b b) (* 4 a c)) 'none))) * But don't over abstract: (define one 1) (define two "two") * Always do test cases (show coverage tool), you might want to comment them, but you should always make sure your code works. * Do not under-document, but also don't over-document. * INDENTATION! (Let DrScheme decide for you, and get used to its rules) --> This is part of the culture that was mentioned last time, but it's done this way for good reason: decades of programming experience have shown this to be the most readable format. * As a general rule, `if' should be either all on one line, or the condition on the first and each consequent on a separate line. Similarly for `define' -- either all on one line or a newline after the object that is being define (either an identifier or a an identifier with arguments). * Another general rule: you should never have white space after an open-paren, or before a close paren (white space includes newlines). Also, before an open paren there should be either another open paren or white space, and the same goes for after a closing paren. * Use the tools that are available to you: for example, use `cond' instead of nested ifs (definitely do not force the indentation to make a nested `if' look like its C counterpart -- remember to let DrScheme indent for you). Another example -- do not use `(+ 1 (+ 2 3))' instead of `(+ 1 2 3)' (this might be needed in *extremely* rare situations, only when you know your calculus and have extensive knowledge about round-off errors). Another example -- do not use `(cons 1 (cons 2 (cons 3 null)))' instead of `(list 1 2 3)'. Also -- don't write things like: (if x #t y) --same-as--> (or x y) (if x y #f) --same-as--> (and x y) (if x #f #t) --same-as--> (not x) * Use these as examples for many of these issues: (define (interest x) (* x (cond [(and (> x 0) (<= x 1000)) 0.04] [(and (> x 1000) (<= x 5000)) 0.045] [else 0.05]))) (define (how-many a b c) (cond ((> (* b b) (* (* 4 a) c)) 2) ((< (* b b) (* (* 4 a) c)) 0) (else 1))) (define (what-kind a b c) (if (equal? a 0) 'degenerate (if (equal? (how-many a b c) 0) 'zero (if (equal? (how-many a b c) 1) 'one 'two) ) ) ) (define (interest deposit) (cond [(< deposit 0) "invalid deposit"] [(and (>= deposit 0) (<= deposit 1000)) (* deposit 1.04) ] [(and (> deposit 1000) (<= deposit 5000)) (* deposit 1.045)] [(> deposit 5000) (* deposit 1.05)])) (define (interest deposit) (if (< deposit 1001) (* 0.04 deposit) (if (< deposit 5001) (* 0.045 deposit) (* 0.05 deposit)))) (define (what-kind a b c) (cond ((= 0 a) 'degenerate) (else (cond ((> (* b b)(*(* 4 a) c)) 'two) (else (cond ((= (* b b)(*(* 4 a) c)) 'one) (else 'none))))))); ======================================================================== An important "discovery" in computer science is that we *don't* need names for every intermediate sub-expression -- for example, in almost any language we can write the equivalent of: s = (-b + sqrt(b^2 - 4*a*c)) / 2a instead of x = b * b y = 4 * a y = y * c x = x - y x = sqrt(x) y = -b x = y + x y = 2 * a s = x / y Such languages are put in contrast to assembly languages, and were all put under the generic label of "high level languages". (Here's an interesting idea -- why not do the same for function values?) ======================================================================== The fact that in Scheme we can use functions as values is very useful in Scheme -- for example, `map', `foldl' & `foldr', many more. Example: ;; every?: (A -> Boolean) (Listof A) -> Boolean ;; Returns false if any element of lst fails the given pred, true if ;; all pass pred. (define (every? pred lst) (or (null? lst) (and (pred (car lst)) (every? pred (cdr lst))))) ======================================================================== Types can become interesting when dealing with higher-order functions. For example, `map' receives a function and a list of some type, and applies the function over this list to accumulate its output, so its type is: ;; map : (A -> B) (Listof A) -> (Listof B) Actually, `map' can use more than a single list, it will apply the function on the first element in all lists, then the second and so on. So the type of `map' with two lists can be described as: ;; map : (A B -> C) (Listof A) (Listof B) -> (Listof C) Here's a hairy example -- what is the type of this function: (define (foo x y) (map map x y)) Begin by what we know -- both `map's, call them `map1' and `map2', have the double- and single-list types of `map' respectively, here they are, with different names for types: ;; the first `map', consumes a function and two lists map1 : (A B -> C) (Listof A) (Listof B) -> (Listof C) ;; the second `map', consumes a function and one list map2 : (X -> Y) (Listof X) -> (Listof Y) Now, we know that `map2' is the first argument to `map1', so the type of `map1's first argument should be the type of `map2': (A B -> C) = (X -> Y) (Listof X) -> (Listof Y) From here we can conclude that A = (X -> Y) B = (Listof X) C = (Listof Y) If we use these equations in `map1's type, we get: map1 : ((X -> Y) (Listof X) -> (Listof Y)) (Listof (X -> Y)) (Listof (Listof X)) -> (Listof (Listof Y)) Now, `foo's two arguments are the 2nd and 3rd arguments of `map1', and its result is `map1's result, so we can now write the type of `foo': ;; foo : (Listof (X -> Y)) ;; (Listof (Listof X)) ;; -> (Listof (Listof Y)) (define (foo x y) (map map x y)) This should help you understand why, for example, this will cause a type error: (foo (list add1 sub1 add1) (list 1 2 3)) and why this is value: (foo (list add1 sub1 add1) (map list (list 1 2 3))) ======================================================================== Quick overview: * map (+ impl) * append, reverse -- run-time! ========================================================================