CSE341 Notes for Wednesday, 2/14/07

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 a b)
          (if (> a b)
              ()
              (cons a (range (+ a 1) b))))
We explored lots of wrong ways to write this and found that you need just the right number of parentheses. For example, if we put parens around 1 to form (1), Scheme interpreted that as a call on a procedure 1. When we left out parentheses, Scheme was unable to group the parts of the expression.

Then we talked about defining a procedure 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. 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)))
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 (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. 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 (f2 x y)
          (let* ((sum (+ x y))
                (sum2 (+ sum 2)))
            (* sum sum sum2 sum2)))
Then we examined a third variation that we need for defining helper procedures. 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(1) and fib(2) returning 1 and a recursive case returning the sum of the two previous values:

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

        > (map fib (range 1 10))
        (1 1 2 3 5 8 13 21 34 55)
But it didn't work well for larger values of n. We asked for:

        > (fib 100)
We had to interrupt this because it didn't finish executing. 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(100)
                      /      \
                f(99)    +     f(98)
                /   \          /   \
            f(98) + f(97)  f(97) + f(96)
            /   \          /   \
        f(97) + f(96) ...
Notice that f(97) 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. I was disappointed that few people could show us this technique for the pow2 function on the midterm. I will be asking a question like this on the final, so people need to learn this technique.

To define a local function, we bind a symbol to a lambda. You might think to use a let or let*, but neither will work in this case. To define a recursive helper procedure, we need to use a variation known as letrec. So the basic form will be:

        (define (fib2 n)
          (if (or (= n 1) (= n 2))
              1
              (letrec ((explore (lambda ?)))
                (explore ?))))
I suggested that we have three parameters: a parameter m indicating which element of the sequence to compute next and accumulators prev and prevprev that indicate the previous two values from the sequence (the values that come before m). For example, we will start by calling:

        (explore 3 1 1)
This says to explore starting with m of 3 and we'll indicate that the values just before the third value are 1 and 1. We want a series of calls that build up the answer step by step:

        (explore 3 1 1)
        calls (explore 4 2 1)
        calls (explore 5 3 2)
        calls (explore 6 5 3)
        calls (explore 7 8 5)
        calls (explore 8 13 8)
        ...
This is very much like an iterative solution, but instead of having a set of local variables that change in value as the loop iterates, we pass parameters that capture the current state of the computation as we iterate through the sequence.

We implemented this strategy with the following procedure:

        (define (fib2 n)
          (if (or (= n 1) (= n 2))
              1
              (letrec ((explore (lambda (m prev prevprev)
                                  (if (= m n)
                                      (+ prev prevprev)
                                      (explore (+ m 1) (+ prev prevprev) prev)))))
                (explore 3 1 1))))
Using this version, we got the same answers as before:

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

        > (fib2 100)
        354224848179261915075
        > (fib2 200)
        280571172992510140037611932413038677189525
        > (fib2 300)
        222232244629420445529739893461909967206666939096499764990979600
You can think of let, let* and letrec as a spectrum where each is more powerful than the previous. I suggested that people use letrec for defining helper procedures even though you can sometimes get by with just a let or let*.

Someone asked why Scheme has these variations. 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 letrec if all you need is a let?


Stuart Reges
Last modified: Mon Feb 19 18:41:38 PST 2007