CSE413 Notes for Friday, 2/16/24

I spent a little time mentioning specific Racket/Scheme procedures that might be helpful for the homework. There is a nice language summary linked from the class web page in notes written by Alan Borning, who used to teach cse341 that would also be helpful to review while working on the homework.

I first showed a procedure called member that can be used to find out whether a particular value is in a list:

        > (member 3 '(7 8 9 10))
        #f
It doesn't follow the usual Scheme convention of having a ? in its name because it is a predicate. That is because it serves double duty. It can be used as a predicate, but it also gives the functionality of a "find" function, returning the part of the list that begins with the value you are searching for:

        > (member 8 '(7 8 9 10))
        '(8 9 10)
There is a convention in Scheme that anything other than #f is considered to be true, so you don't need to return #t to indicate that something is true.

Then I mentioned that there are procedures for computing logical and and logical or:

        > (and (< 3 4) (= 2 (- 4 2)))
        #t
        > (or (> 3 4) (= 2 3) (>= (+ 2 2) 3))
        #t
They give us short-circuited evaluation, which means that as soon as it has produced a value, it stops evaluating the successive terms, as in:

        > (and (< 3 4) (= 2 (- 4 2)) (< 4 3) (= 10 (/ 1 0)))
        #f
We saw that there wasn't a division by zero error produced if one of the preceding expressions allowed Scheme to determine the result without considering that later term (as in (< 4 3) evaluating to #f)).

I mentioned that there are procedures called andmap and ormap that are like and and or but they take a predicate to be applied to each value in the list:

        > (andmap number? '(3 8 5 a b))
        #f
I said that the append procedure is equivalent to the OCaml @ operator and is actually more flexible because it takes an indefinite number of arguments to append together:

        > (append '(3 4 5) '(10 20 30) '(a b c))
        '(3 4 5 10 20 30 a b c)
I reminded people that you can use the list procedure to form a list and the length procedure to ask for the length of a list:

        > (define a (list 3 4 5 'a))
        > a
        '(3 4 5 a)
        > (length a)
        4
And I mentioned that for the homework we will be using a procedure called random that returns a random real value n with 0 <= n < 1:

        > (random)
        0.9876574623474741
        > (random)
        0.06153977378277876
        > (random)
        0.7587938671533774
If you provide an integer parameter n to the call on random, it will produce an integer between 0 and n-1:

        > (random 10)
        8
        > (random 10)
        2
        > (random 10)
        5
Then I switched topics to point out an important difference between Scheme and OCaml. I typed the following into Scheme and got the expected result:

        > (define x 4)
        > (define (f n) (+ x n))
        > (f 6)
        10
This procedure 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 OCaml, we found that the procedure was unchanged. But that wasn't the case in Scheme:

        > (define x 12)
        > (f 6)
        18
So in OCaml, 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 procedure, 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 procedure in terms of a variable and a procedure that haven't been defined:

        > (define (g n) (+ x y n (h n)))
This defines a procedure 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 procedure 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)
        y: undefined
        > (define y 7)
        > (g 6)
        h: undefined
        > (define (h n) (* n 3))
        > (g 6)
        43
Because Scheme is designed this way, it is easier to define mutually recursive procedures and you have more flexibility about the order you can use for defining procedures 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 OCaml, 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 procedure 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 procedure +, we eval it:

        > (eval b)
        #<procedure:+>
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 "expression? ")
          (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)
        expression? 2.8
        2.8 --> 2.8
        expression? (+ 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 "expression? ")
          (let ((exp (read)))
            (display exp)
            (display " --> ")
            (display (eval exp))
            (newline)
            (repl)))
This had the usual behavior of the read-eval-print loop:

        > (repl)
        expression? 3.4
        3.4 --> 3.4
        expression? (+ 2 2)
        (+ 2 2) --> 4
        expression? (* 3.4 (+ 7 9) (- 18 4))
        (* 3.4 (+ 7 9) (- 18 4)) --> 761.6
There is a similar procedure called apply that takes two arguments: a procedure 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 OCaml. 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 OCaml 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.

Then I asked people how to write a function called repeat that would call a function n times. I wanted to use it to see the results of calls on the random procedure without having to keep making the calls one at a time. We first wrote it this way:

        > (define (repeat f n) (if (= n 0) '() (cons f (repeat f (- n 1)))))
        > (repeat (lambda () (random 10)) 20)
This returned a list that had 20 occurrences of #<procedure>. Someone suggested that we eval f:

        > (define (repeat f n) (if (= n 0) '() (cons (eval f) (repeat f (- n 1)))))
This behaved the same way. One approach would be to use apply instead of eval, but it is even easier to just put parentheses around f to actually call the procedure:

        > (define (repeat f n) (if (= n 0) '() (cons (f) (repeat f (- n 1)))))
        > (repeat (lambda () (random 10)) 20)
        '(0 2 7 3 5 2 1 9 0 3 3 4 7 9 1 1 9 0 5 3)

Stuart Reges
Last modified: Sat Feb 17 14:54:27 PST 2024