CSE341 Notes for Wednesday, 5/6/09

We continued our discussion of Scheme. We began by implementing a procedure called range that creates a list of sequential integers in a particular range (the Scheme equivalent of the -- operator we defined in ML):

        (define (range x y)
          (if (> x y)
              ()
              (cons x (range (+ x 1) y))))
Then we talked about defining a procedure that will compute (x + y)2. We can do this directly, as in:

        (define (f 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 ML's let/in/end construct. It takes a list of symbol/expression pairs and an expression to evaluate:

(let ((<symbol> <expr>) ... (<symbol> <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 procedure f, I purposely made a parenthesis mistake with the let:

        (define (f 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 (f x y)
          (let ((sum (+ x y)))
            (* sum sum)))
I then asked how we could define a second procedure that would compute (x + y)2 times (x + y + 2)2. We could again define this directly:

        (define (f 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 (f 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. The problem is that the let expression doesn't work the way it did in ML. In particular, 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 (f x y)
          (let* ((sum (+ x y))
                (sum2 (+ sum 2)))
            (* sum sum sum2 sum2)))
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.

Previous versions of cse341 have encouraged students to use letrec for defining local helper procedures, 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 procedures as an "internal definition" (embedding a define inside a define).

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

        (define (f x y z)
          (define (g n)
            (- (sqr n) 2))
          (+ (g x) (g y) (g z)))
To explore this further, I asked people how to write a procedure for computing the Fibonacci sequence. The Fibonacci sequence begins with the numbers 1, 1 and then each successive number is the sum of the previous two:

1, 1, 2, 3, 5, 8, 13, 21, 34
We can define this with a base case for fib(0) and fib(1) returning 1 and a recursive case returning the sum of the two previous values:

        (define (fib n)
          (if (<= n 1)
              1
              (+ (fib (- n 1)) (fib (- n 2)))))
This worked fine for small values of n:

        > (map fib (range 0 10))
        (1 1 2 3 5 8 13 21 34 55 89)
But it didn't work well for larger values of n. We waited a long time for it to compute values just up to 40.

The problem is that written in the simple way, the fib procedure computes the same values over and over again. Here is just a small part of the computation:

                        f(40)
                      /       \
                f(39)     +     f(38)
                /   \           /   \
            f(38) + f(37)   f(37) + f(36)
            /   \           /   \
        f(37) + f(36) ...
Notice that f(37) is computed 3 different times. This gets exponentially worse for lower values of n. In fact, the number of calls made by this procedure is itself a slight variation of the Fibonacci sequence. It grows exponentially as the Fibonacci sequence itself does.

If we wanted to solve this iteratively, we'd introduce some local variables to keep track of previous values of the sequence. As I've mentioned several times, we can do something similar with functional programming by writing a helper procedure that has extra parameters. These are sometimes referred to as accumulators because they are used to accumulate part of the answer.

So we introduced an internal definition for a procedure that will perform the kind of iterations the loop would perform. The loop would keep track of how many iterations to perform and the two previous values from the sequence, so we decided to have our helper procedure look like this:

        (define (fib n)
          (define (iterate times prev prevPrev)
           ...
We decided that we'd stop iterating when times became 1. In that case, we can simply return the sum of the two previous numbers:

        (define (fib n)
          (define (iterate times prev prevPrev)
            (if (<= times 1)
                (+ prev prevPrev)
                ...
But we realized that we're going to need the sum for the recursive call as well, so it makes sense to factor this out using let:

        (define (fib n)
          (define (iterate times prev prevPrev)
            (let ((sum (+ prev prevPrev)))
              (if (<= times 1)
                  sum
                  ...
In the recursive case, we simply want to do the next iteration. In our loop we would decrement the counter, replace prev with sum and replace prevPrev with the old value of prev. This can be accomplished by making a recursive call with those updated values:

        (define (fib n)
          (define (iterate times prev prevPrev)
            (let ((sum (+ prev prevPrev)))
              (if (<= times 1)
                  sum
                  (iterate (- times 1) sum prev))))
          ...
This completed the definition of the helper procedure. Now we had to add the expression that indicates the value to return for the overall procedure. It includes the same base case and in the recursive case, it calls the helper procedure with appropriate initial values for the counter, prev and prevPrev:

        (define (fib n)
          (define (iterate times prev prevPrev)
            (let ((sum (+ prev prevPrev)))
              (if (<= times 1)
                  sum
                  (iterate (- times 1) sum prev))))
	  (iterate n 1 0))
 
Using this version, we got the same answers as before:

        > (map fib (range 0 10))
        (1 1 2 3 5 8 13 21 34 55 89)
And we were able to compute the later terms of the sequence:

        > (fib 100)
        573147844013817084101
        > (fib 200)
        453973694165307953197296969697410619233826
        > (fib 300)
        359579325206583560961765665172189099052367214309267232255589801
I pointed out that we can define helper procedures 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 procedure. 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.

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?

I also pointed out that in the Scheme interpreter, you can get the previous line you typed in the interpreter by giving the emacs command "M-p" (meta-P). "P" is short for "previous." Some terminal emulators will bind keys like alt to be a meta key, but I personally get this by typing two keys in a row: Esc followed by p.


Stuart Reges
Last modified: Wed May 13 15:33:18 PDT 2009