CSE341 Notes for Friday, 5/15/09

I spent most of the lecture discussing how mutation works in Scheme. We've seen that you can call define to bind and rebind a value:

        > (define x 34)
        > x
        34
        > (define x 18.9)
        > x
        18.9
But this all takes place at the top level environment. For example, if I have a function that tries to rebind x, it doesn't work:

        > (define (foo) (define x 13) x)
        > (foo)
        13
        > x
        18.9
In this case it defines a local x that has value 13, but that has no effect on the global x in the top-level environment.

There is an alternative. You need to use define to introduce new identifiers into the environment, but once introduced, you can call set! to change their values. The convention in Scheme is to have an exclamation mark at the end of the name of any function that is a mutating function. So if we had written our local function using set! instead, then we end up changing the actual global variable:

        > (define (foo) (set! x 13) x)
        > (foo)
        13
        > x
        13
I mentioned that it's good to draw pictures to keep track of what's going on in these situations. Remember that Scheme uses a pointer paradigm, like Java's objects. For example, when we say:
        (define x '(1 2 3))
        (define y x)
We end up with this situation with x and y both pointing at the same list:

          x     y
          |     |
          V     V
        +-----+-----+    +-----+-----+    +-----+-----+
        |  +  |  +--+--> |  +  |  +--+--> |  +  |  +--+--> ()
        +-/---+-----+    +-/---+-----+    +-/---+-----+
         /                /                /
        1                2                3
When we then executed this code:

        (set! x '(12 13))
We reset x to point to a new list with y still pointing to the old list:

          x
          |
          V
        +-----+-----+    +-----+-----+
        |  +  |  +--+--> |  +  |  +--+--> ()
        +-/---+-----+    +-/---+-----+
         /                /
        12               13

          y
          |
          V
        +-----+-----+    +-----+-----+    +-----+-----+
        |  +  |  +--+--> |  +  |  +--+--> |  +  |  +--+--> ()
        +-/---+-----+    +-/---+-----+    +-/---+-----+
         /                /                /
        1                2                3
Scheme tries to avoid making copies of lists when it doesn't need to, but if you clearly ask for a second version of a list, then Scheme will oblige. So in executing this code:

        (define x '(1 2 3))
        (define y x)
        (define z '(1 2 3))
We end up with two lists, one pointed to by x and y and one pointed to by z:

          x     y
          |     |
          V     V
        +-----+-----+    +-----+-----+    +-----+-----+
        |  +  |  +--+--> |  +  |  +--+--> |  +  |  +--+--> ()
        +-/---+-----+    +-/---+-----+    +-/---+-----+
         /                /                /
        1                2                3

          z
          |
          V
        +-----+-----+    +-----+-----+    +-----+-----+
        |  +  |  +--+--> |  +  |  +--+--> |  +  |  +--+--> ()
        +-/---+-----+    +-/---+-----+    +-/---+-----+
         /                /                /
        1                2                3
We had seen this earlier when we did calls on eq? and found that x and y are considered equal but not x and z. That's because eq? does a pointer-level comparison (are these the exact same object?).

In addition to the set! function, the Scheme standard includes functions called set-car! and set-cdr! that can be used to perform surgery on lists. The pictures above show "cons cells" (cells that have both a car and a cdr pointer). The functions set-car! and set-cdr! are used to change these individual pointers.

For example, given the initial situation above, we made this call:

        (set-car! x 17)
This goes to the cons cell that x points to and resets its car pointer to point to the value 17. It has no effect on the list that z points to although y still points to the same list as x, so it is affected:

          x     y
          |     |
          V     V
        +-----+-----+    +-----+-----+    +-----+-----+
        |  +  |  +--+--> |  +  |  +--+--> |  +  |  +--+--> ()
        +-/---+-----+    +-/---+-----+    +-/---+-----+
         /                /                /
        17               2                3
To be able to do this, we had to switch our language in DrScheme to be R5RS. The designers of "Pretty Big" have decided to eliminate these list surgery functions from their variation of Scheme. So in the "Pretty Big" variant that we're using, normal lists are immutable, just as they are in ML. You can build up mutable lists by making calls on a function called mcons and the mutating functions are called set-mcar! and set-mcdr!.

We then looked at some examples that involve append:

        (define x '(1 2 3))
        (define y x)
        (define z '(1 2 3))

        (define a '(a b))
        (define b (append a x))
        (define c (append a y))
        (define d (append a z))
Scheme, like ML, will try to avoid copying when it doesn't need to. In this case, it needs to make a copy of the first argument to append, but it doesn't need to copy the second argument. So when we asked whether any of b, c, and d were eq? to each other, the answer was no. But we found that b and c shared a substructure because the second argument to append is not copied:

        > (eq? (caddr b) (caddr c))
        #t
I mentioned that we would have a final exam question that involves understanding this.

I then talked about how the mutating functions can be used to create a local variable that only certain functions have access to:

        (define incr null)
        (define get null)
        (define m 3)
        (let ((n 0))
          (set! incr (lambda (i) (set! n (+ n i m))))
          (set! get (lambda () n)))
We saw that the get function would report the value of n and the incr function was able to increment n, but we can't access n from the top-level environment:

        > (get)
        0
        > (incr 3)
        > (get)
        6
        > (incr 5)
        > (get)
        14
        > n
        * reference to undefined identifier: n
The call on let introduces a local scope in which n is declared. Each lambda that appears inside the let causes Scheme to set up a closure that has a reference to that inner environment that has the variable n.

We then spent time discussing a technique known as memoization in which we remember what values were returned by various calls on a function. The idea is similar to the idea of caching. As a function is called, we keep track of what values were returned for each call, memoizing the result. This is a useful technique that can be used in any programming language. It is particularly helpful for speeding up recursive definitions that compute the same value multiple times.

In a previous lecture we wrote an inefficient version of the fib function for computing Fibonacci numbers:

        (define (fib n)
          (if (<= n 1)
              1
              (+ (fib (- n 1)) (fib (- n 2)))))
The complexity of this function is exponential. We were able to rewrite it using a more iterative approach, but we can use memoization instead. We set up a global variable called answers with the first two values:

        (define answers '((0 . 1) (1 . 1)))
Then in writing the function, we first did a lookup against our list of answers. If we've already computed that Fibonacci number, then we just return its value. Otherwise we compute an answer and store it in our list of answers so that we never compute it again:

        (define (fib n)
          (let ((match (assoc n answers)))
            (if match
                (cdr match)
                (let ((new-answer (+ (fib (- n 1)) (fib (- n 2)))))
                  (begin (set! answers (cons (cons n new-answer) answers))
                         new-answer)))))

Stuart Reges
Last modified: Tue May 19 13:25:32 PDT 2009