> (define x 4) > (define (f n) (+ x n)) > (f 6) 10This 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) 18So 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) 43Because 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) 7But 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) 7Similarly, 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 "You rang? ") (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) You rang? 2.8 2.8 --> 2.8 You rang? (+ 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 "You rang? ") (let ((exp (read))) (display exp) (display " --> ") (display (eval exp)) (newline) (repl)))This had the usual behavior of the read-eval-print loop:
> (repl) You rang? 3.4 3.4 --> 3.4 You rang? (+ 2 2) (+ 2 2) --> 4 You rang? (* 3.4 (+ 7 9) (- 18 4)) (* 3.4 (+ 7 9) (- 18 4)) --> 761.6There 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.5We 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.
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 4While in this case we see just a call on work with the parameter 4:
> (if (> 2 3) (work 3) (work 4)) work called: 4 5We 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 5The 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 5Notice 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) 4You 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!4>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) 4We 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 5Notice 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.
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. It also includes a call on a built-in method Arrays.asList that has a varargs parameter, which allows you to enumerate the values you want to include.
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.