CSE341 Notes for Wednesday, 5/1/24

We continued our discussion of Scheme. We began by writing the map function in Scheme.
        (define (map f lst)
          (if (null? lst)
              '()
              (cons (f (car lst)) (map f (cdr lst)))))
To write this, we needed a predicate to test for an empty list. We used the "null?" predicate (pronounced by Scheme programmers as "null p" or "null huh?") which tells you whether or not something is an empty list.

Then we wrote the Scheme version of filter. This function involves a 3-way branch (empty list, first value passes the test, first value fails the test). For a 3-way branch, it is better to use a construct known as cond. With cond you can pick between any number of alternatives. It is like a nested if/else construct in Java or OCaml (if, else if, else if, else if, else). It has this general form:

(cond (<test> <exp>) (<test> <exp>) ... (<test> <exp>)) If you want a final "else" clause, the convention is to use the word "else" for the final test. As with a nested if/else, it sequentially performs each test and returns the expression that corresponds to the first test that returns true.

Using cond, we wrote our version of filter as follows:

        (define (filter f lst)
          (cond ((null? lst) '())
                ((f (car lst)) (cons (car lst) (filter f (cdr lst))))
                (else (filter f (cdr lst)))))
Then we wrote a function that would convert a list into a list of lists that are 2-long. It pairs up values to make sublists, as in:

        > (pair-off (range 1 11))
        ((1 2) (3 4) (5 6) (7 8) (9 10))
If the list has an odd number of elements, it makes a sublist out of the final element:

        > (pair-off (range 1 12))
        ((1 2) (3 4) (5 6) (7 8) (9 10) (11))
To write this we need to be able to make a list. Scheme has a function called list that does exactly that:

        > (list 1 3 4)
        '(1 3 4)
We again used a cond for this code and we used the length of the list to test for the 1-element case and the list function to make lists:

      (define (pair-off lst)
        (cond ((null? lst) '())
              ((= (length lst) 1) (list lst))
              (else (cons (list (car lst) (cadr lst)) (pair-off (cddr lst))))))
Then we talked about defining a function that will compute (x + y)2. We can do this directly, as in:

        (define (f1 x y)
          (* (+ x y) (+ x y)))
We didn't like the idea of computing the sum twice, so we decided to include a let expression to define a local variable called sum. The let construct is similar to OCaml's let/in/end construct. It takes a list of symbol/expression pairs and an expression to evaluate:

(let ([&lt;symbol&gt; <expr>] ... [&lt;symbol&gt; <expr>]) <expr>) The symbol/expression pairs are like local variable definitions, so this is saying, "Let these symbols be bound to what these expressions evaluate to in evaluating the following expression."

When I tried to use this in our function f1, I purposely made a parenthesis mistake with the let:

        (define (f1 x y)
          (let (sum (+ x y))
            (* sum sum)))        
This didn't work because the first argument of a let should be a list of bindings. So even if there is just one variable being defined, we have to put that binding inside a list:

        (define (f1 x y)
          (let ((sum (+ x y)))
            (* sum sum)))
DrRacket allows you to use square brackets for the bindings to try to avoid this mistake:
        (define (f x y)
          (let ([sum (+ x y)])
            (* sum sum)))
In fact, if you type the keys for brackets as you do this, DrRacket will correctly use parentheses when appropriate and brackets when appropriate. This can be a useful habit to prevent making errors.

I then asked how we could define a second function that would compute (x + y)2 times (x + y + 2)2. We could again define this directly:

        (define (f2 x y)
          (let ([sum (+ x y)])
            (* sum sum (+ sum 2) (+ sum 2))))
But we again decided to use a local variable to avoid computing the second sum twice:

        (define (f2 x y)
          (let ([sum (+ x y)]
                [sum2 (+ sum 2)])
            (* sum sum sum2 sum2)))
Unfortunately, this didn't work. It generated a runtime error in the binding of sum2, saying that sum is not defined. As in OCaml, you can't refer to other definitions included in the let expression. The way to get around this is to use a variation known as let* which allows you to refer to other bindings:

        (define (f2 x y)
          (let* ([sum (+ x y)]
                 [sum2 (+ sum 2)])
             (* sum sum sum2 sum2)))
Order matters in this case, so it is important to define sum before using it.

You can think of let and let* as being two points on a spectrum. The let construct is the simplest. It says to compute various local variables without paying attention to the other local variables being defined. The let* construct says to define them sequentially so that a later local variable definition can make reference to an earlier one. There is a third variation of let that is known as letrec that is even more powerful than let*, but we won't need it for what we'll be doing.

Someone asked why Scheme has variations like let, let* and letrec. I said that I've heard many answers from Scheme programmers. The most common answer is that Scheme programmers want their code to be efficient so they don't want to be forced to use a more expensive operation if they don't need to. Why should you have to pay the price of a let* if all you need is a let?

Some instructors encourage students to use letrec for defining local helper functions, but I think this syntax is unnecessarily confusing. Following the advice that Abelson and Sussman give in Structure and Interpretation of Computer Programs, we will define local helper functions as an "internal definition" (embedding a define inside a define).

For example, the function f3 below computes (x2 - 2) + (y2 - 2) + (z2 - 2) by calling a local function g that computes (n2 - 2):

        (define (f3 x y z)
          (define (g n)
            (- (sqr n) 2))
          (+ (g x) (g y) (g z)))
I pointed out that we can define helper functions using internal definitions because the syntax of define is really this:

(define (<name> <param> <param> ... <parm>) <exp1> <exp2> ... <expn>) In other words, you can include many expressions to be evaluated. Scheme uses the value of the final expression as the overall value of the function. I told people not to abuse this ability. You can run into trouble if you use internal definitions to define local variables, so we'll continue to use let and let* for those.

I mentioned that Racket has a way to define a structured object with named fields. You call the "define-struct" procedure, as in the following definition of a point structure that has named fields called x and y:

        (define-struct point (x y))
The net effect of calling this procedure is that we have a number of useful procedures defined for us, including:

For example:

        > (define-struct point (x y))
        > (define p (make-point 2 15))
        > (point? p)
        #t
        > (point? '(2 15))
        #f
        > (point-x p)
        2
        > (point-y p)
        15
We will be using a struct for the first Scheme homework assignment.


Stuart Reges
Last modified: Wed May 1 14:41:39 PDT 2024