CSE341 Notes for Monday, 5/18/09

We finished the discussion of the fibonacci example. We had ended up with the following code:

        (define answers '((0 . 1) (1 . 1)))
        (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)))))
We saw that as we asked for higher values of fib, our global variable ended up with more memoized results:

        > answers
        ((0 . 1) (1 . 1))
        > (fib 5)
        8
        > answers
        ((5 . 8) (4 . 5) (3 . 3) (2 . 2) (0 . 1) (1 . 1))
        > (fib 20)
        10946
        > answers
        ((20 . 10946)
         (19 . 6765)
         (18 . 4181)
         (17 . 2584)
         (16 . 1597)
         (15 . 987)
         (14 . 610)
         (13 . 377)
         (12 . 233)
         (11 . 144)
         (10 . 89)
         (9 . 55)
         (8 . 34)
         (7 . 21)
         (6 . 13)
         (5 . 8)
         (4 . 5)
         (3 . 3)
         (2 . 2)
         (0 . 1)
         (1 . 1))
We then looked at how to localize answers. Instead of using a global variable, we can use a let inside the function and define a helper function that has access to it:

        (define (fib n)
          (let ((answers '((0 . 1) (1 . 1))))
          (define (helper n)
            (let ((match (assoc n answers)))
              (if match
                  (cdr match)
                  (let ((new-answer (+ (helper (- n 1)) (helper (- n 2)))))
                    (begin (set! answers (cons (cons n new-answer) answers))
                           new-answer)))))
           (helper n)))
This is a pretty good answer in that every time you call fib, it sets up a variable called answers that memoizes the results. But we can do even better. Why reconstruct answers every time you call fib? If we instead want to construct the answers just once, then we don't want fib to be defined as a normal function as we have done above. Instead, we want to set it up just once and make it a function that has access to answers. But we've already done that. Our helper function is the function we want fib to be, so all we have to do is assign fib to be the helper:

        (define fib
          (let ((answers '((0 . 1) (1 . 1))))
          (define (helper n)
            (let ((match (assoc n answers)))
              (if match
                  (cdr match)
                  (let ((new-answer (+ (helper (- n 1)) (helper (- n 2)))))
                    (begin (set! answers (cons (cons n new-answer) answers))
                           new-answer)))))
            helper))
This assignment happens just once, so that we construct the answers just once. That means that we'll never end up computing the same value of fib more than once.

Then I discussed how to define a function with a variable number of arguments in Scheme. This concept is sometimes referred to as varargs (short for "variable arguments"). We looked at the following java program that includes a

        // short program to demonstrate variable number of arguments for a method.
        
        import java.util.*;
        
        public class Varargs {
            public static void main(String[] args) {
        	printAll(3, 8, 19.4, "hello");
        	printAll(74.5, "hi", 19);
        	List<Integer> data = Arrays.asList(3, 19, 24, 79, 202);
        	System.out.println(data);
            }
        
            public static void printAll(Object... data) {
        	for (int i = 0; i < data.length; i++)
        	    System.out.println(data[i]);
        	System.out.println();
            }
        }
It defines a method printAll that takes an indefinite number of objects. Java provides the arguments in the form of an array.

In Scheme you can either indicate exactly how many parameters a function has and it will enforce that number or you can indicate that it has an indefinite number of parameters (zero or more). You choose the one or the other by either including parentheses or not when you define a function using the lambda form, as in:

        (define f1 (lambda (n) (+ n 1)))
        (define f2 (lambda n (display n)))
We saw that f1 required exaclty one parameter while f2 would take an indefinite number of parameters. In that case, the parameters are provided to f2 as a list.

Then I said that I wanted to look at various examples that involved delayed evaluation of code. To explore this, we began with the following function that displays a message every time it is called:

        (define (work x)
          (display "work called: ")
          (display x)
          (newline)
          (+ x 1))
For example, we have discussed the fact that if does not evaluate its third and fourth arguments unless it has to. Notice how in this call we see a message only for the call on work with the parameter 3:

        > (if (< 2 3) (work 3) (work 4))
        work called: 3
        4
While in this case we see just a call on work with the parameter 4:

        > (if (> 2 3) (work 3) (work 4))
        work called: 4
        5
We considered what happens when you write a function with a similar set of parameters to if (a test, a first value and a second value):

        (define (test t e1 e2)
          (if t
              (+ e1 e1 e1)
              e2))
As we've discussed, when you write a simple function like this, Scheme will fully evaluate both parameters once before executing the function, so it's not surprising that we see both messages when we call it:

        > (test (< 2 3) (work 3) (work 4))
        work called: 3
        work called: 4
        12
        > (test (> 2 3) (work 3) (work 4))
        work called: 3
        work called: 4
        5
The first variation I discussed was the idea of delaying evaluation by making these parameters thunks. A thunk is a function of zero arguments (a lambda) that is used to wrap up an expression to delay its evaluation. So for the test function, instead of taking parameters e1 and e2 that are already evaluated, we assume they are thunks (lambdas of zero arguments) that need to be called:

        (define (test2 t e1 e2)
          (if t
              (+ (e1) (e1) (e1))
              (e2)))
In calling the test2 function, we now have to wrap up each expression in a lambda:

        > (test2 (< 2 3) (lambda () (work 3)) (lambda () (work 4)))
        work called: 3
        work called: 3
        work called: 3
        12
        > (test2 (> 2 3) (lambda () (work 3)) (lambda () (work 4)))
        work called: 4
        5
Notice that we have duplicated one of the properties that if has in that we only evaluate the parameter that we end up being interested in. But notice that for e1, we end up evaluating it three different times.

Another way to approach this is to use the built in functions called delay and force. They operate on a data type known as a "promise". For example:

        > (define x (delay (+ 2 2)))
        > x
        #<struct:promise>
This says to delay the evaluation of the expression until later. You then request the evaluation by calling force:

        > (force x)
        4
You might think that after calling this that x has now been set to 4, but that's not true. x still refers to the promise:

        > x
        #<struct:promise>
But you can always get the value again by calling force and, as we'll see, Scheme uses memoization to ensure that the code is not evaluated multiple times:

        > (force x)
        4
We rewrote the test function to use calls on force:

        (define (test3 t e1 e2)
          (if t
              (+ (force e1) (force e1) (force e1))
              (force e2)))
When we called it, we now had to wrap up the expressions in a call on delay, as in:

        > (test3 (< 2 3) (delay (work 3)) (delay (work 4)))
        work called: 3
        12
        > (test3 (> 2 3) (delay (work 3)) (delay (work 4)))
        work called: 4
        5
Notice that with the combination of force and delay, we have the same properties that we had with if. We only evaluate arguments if we need them and we only evaluate them once, even if the result is referred to multiple times.


Stuart Reges
Last modified: Sun Jun 7 13:23:16 PDT 2009