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)