CSE341 Notes for Monday, 5/11/09

I began by pointing out an important difference between Scheme and ML. I typed the following into Scheme and got the expected result:

        > (define x 4)
        > (define (f n) (+ x n))
        > (f 6)
        10
This function definition is making reference to a free variable x, which picks up the value 4 from the top-level environment. I then asked people what would happen if we were to redefine x. In ML, we found that the function was unchanged. But that wasn't the case in Scheme:

        > (define x 12)
        > (f 6)
        18
So in ML, the closure that is formed for a function keeps track of the bindings that existed when the function was defined. Some people wondered whether Scheme is using dynamic scope, but that's not the case. In fact, Scheme was the first Lisp dialect to use lexical scoping. So when Scheme forms a closure for a function, it keeps a reference to the scope in which this is defined (the top-level environment), but it doesn't keep track of the state of that set of bindings. So if we rebind x to a different value, Scheme will use the new value rather than the old value.

Someone asked whether this works with elements that haven't even been defined. The answer is yes. For example, we can define a function in terms of a variable and a function that haven't been defined:

        > (define (g n) (+ x y n (h n)))
This defines a function g in terms of a parameter n, a free variable x that is currently defined in the top-level environment, a free variable y that hasn't been defined yet, and a function h that hasn't been defined yet.

Of course, we can't call g without generating an error until we've defined both y and h:

        > (g 6)
        reference to undefined identifier: y
        > (define y 7)
        > (g 6)
        reference to undefined identifier: h
        > (define (h n) (* n 3))
        > (g 6)
        43
Because Scheme is designed this way, it is easier to define mutually recursive function and you have more flexibility about the order you can use for defining functions and variables. Of course, the cost is that we get less predictable behavior because we might redefine variables or functions, but it simplifies the syntax of the language. In ML, for example, we had a special "and" construct for defining mutually recursive functions. Scheme does not need such a construct.

I then spent some time discussing how to implement a repl (read/eval/print loop). In Scheme, the distinction between data and code is very loose. We know that we use list notation to tell the interpreter to call built-in functions like +:

        > (+ 3 4)
        7
But that same list can be stored as data, as in:

        > (define a '(+ 3 4))
        > a
        (+ 3 4)
The symbol a stores a reference to a list that could be thought of as storing data, but could also be thought of as code that can be executed. In Scheme, you can execute such code by evaluating it with the function eval:

        > (eval a)
        7
Similarly, we can set a variable to refer to the symbol +:
        > (define b '+)
        > b
        +
And if we ever want to turn the symbol + into the function +, we eval it:

        > (eval b)
        #<primitive:+>
I then asked people to think about how the top-level read-eval-print loop is written. I first wrote it this way:

        (define (repl)
          (display "what is your bidding, master? ")
          (let ((exp (read)))
            (display exp)
            (display " --> ")
            (display exp)
            (newline)
            (repl)))
This sets up a continuous loop that prompts, then reads an expression, then echos the expression. It behaved like this:

        > (repl)
        what is your bidding, master? 2.8
        2.8 --> 2.8
        what is your bidding, master? (+ 2 2)
        (+ 2 2) --> (+ 2 2)
Of course, the actual read-eval-print loop evaluates expressions like (+ 2 2). It was easy to change our code to do this by calling eval on the expression:

        (define (repl)
          (display "what is your bidding, master? ")
          (let ((exp (read)))
            (display exp)
            (display " --> ")
            (display (eval exp))
            (newline)
            (repl)))
This had the usual behavior of the read-eval-print loop:

        > (repl)
        what is your bidding, master? 3.4
        3.4 --> 3.4
        what is your bidding, master? (+ 2 2)
        (+ 2 2) --> 4
        what is your bidding, master? (* 3.4 (+ 7 9) (- 18 4))
        (* 3.4 (+ 7 9) (- 18 4)) --> 761.6
There is a similar function called apply that takes two arguments: a function and a list of arguments, as in:

        > (apply + '(3 8 14.5))
        25.5
We don't have this kind of capability in Java or ML. For example, it might be nice to say something like this in Java:

        String s = "System.out.println(48);";
        execute(s);
Java doesn't have any such capability. It is much more difficult in a statically typed language like Java or ML to dynamically execute code like this at runtime. We'd have to somehow invoke the compiler to check the types of values mentioned in the expression. But in a language like Scheme that is designed to do its type checking at runtime, it is much easier to allow this.

I then spent the last part of class talking about a bigger example than we've seen before. I talked about the idea of writing a function that would take the derivative of a function with respect to some variable. This is a classic symbolic computation task where languages like Scheme are likely to outshine languages like Java. I got the idea from the Abelson and Sussman text (Structure and Interpretation of Computer Programs).

We said that we would support the following kinds of expressions:

        ; expression:
        ;    number
        ;    variable
        ;    (+ expression expression)
        ;    (* expression expression)
We didn't have enough time to do this completely, but we were able to write to include cases for numbers, variables and sums. I mentioned that it is a good idea to include helper functions to make the code easier to read and to simplify the main function:

        (define (derivative exp var)
          (cond ((number? exp) 0)
                ((symbol? exp)
                 (if (eq? exp var) 1 0))
                ((sum? exp) (make-sum (derivative (arg1 exp) var)
                                      (derivative (arg2 exp) var)))
                (else (error "illegal expression"))))
        (define (sum? exp)
          (and (pair? exp) (eq? (car exp) '+)))
        (define (arg1 exp) (cadr exp))
        (define (arg2 exp) (caddr exp))
        (define (make-sum exp1 exp2)
          (list '+ exp1 exp2))
I said that we'd explore this further in section.


Stuart Reges
Last modified: Wed May 13 15:32:30 PDT 2009