> (define x 34) > x 34 > (define x 18.9) > x 18.9But this all takes place at the top level environment. For example, if I have a function that tries to rebind x, it doesn't work:
> (define (foo) (define x 13) x) > (foo) 13 > x 18.9In this case it defines a local x that has value 13, but that has no effect on the global x in the top-level environment.
There is an alternative. You need to use define to introduce new identifiers into the environment, but once introduced, you can call set! to change their values. The convention in Scheme is to have an exclamation mark at the end of the name of any function that is a mutating function. So if we had written our local function using set! instead, then we end up changing the actual global variable:
> (define (foo) (set! x 13) x) > (foo) 13 > x 13I mentioned that it's good to draw pictures to keep track of what's going on in these situations. Remember that Scheme uses a pointer paradigm, like Java's objects. For example, when we say:
(define x '(1 2 3)) (define y x)We end up with this situation with x and y both pointing at the same list:
x y | | V V +-----+-----+ +-----+-----+ +-----+-----+ | + | +--+--> | + | +--+--> | + | +--+--> () +-/---+-----+ +-/---+-----+ +-/---+-----+ / / / 1 2 3When we then executed this code:
(set! x '(12 13))We reset x to point to a new list with y still pointing to the old list:
x | V +-----+-----+ +-----+-----+ | + | +--+--> | + | +--+--> () +-/---+-----+ +-/---+-----+ / / 12 13 y | V +-----+-----+ +-----+-----+ +-----+-----+ | + | +--+--> | + | +--+--> | + | +--+--> () +-/---+-----+ +-/---+-----+ +-/---+-----+ / / / 1 2 3Scheme tries to avoid making copies of lists when it doesn't need to, but if you clearly ask for a second version of a list, then Scheme will oblige. So in executing this code:
(define x '(1 2 3)) (define y x) (define z '(1 2 3))We end up with two lists, one pointed to by x and y and one pointed to by z:
x y | | V V +-----+-----+ +-----+-----+ +-----+-----+ | + | +--+--> | + | +--+--> | + | +--+--> () +-/---+-----+ +-/---+-----+ +-/---+-----+ / / / 1 2 3 z | V +-----+-----+ +-----+-----+ +-----+-----+ | + | +--+--> | + | +--+--> | + | +--+--> () +-/---+-----+ +-/---+-----+ +-/---+-----+ / / / 1 2 3We had seen this earlier when we did calls on eq? and found that x and y are considered equal but not x and z. That's because eq? does a pointer-level comparison (are these the exact same object?).
In addition to the set! function, the Scheme standard includes functions called set-car! and set-cdr! that can be used to perform surgery on lists. The pictures above show "cons cells" (cells that have both a car and a cdr pointer). The functions set-car! and set-cdr! are used to change these individual pointers.
For example, given the initial situation above, we made this call:
(set-car! x 17)This goes to the cons cell that x points to and resets its car pointer to point to the value 17. It has no effect on the list that z points to although y still points to the same list as x, so it is affected:
x y | | V V +-----+-----+ +-----+-----+ +-----+-----+ | + | +--+--> | + | +--+--> | + | +--+--> () +-/---+-----+ +-/---+-----+ +-/---+-----+ / / / 17 2 3To be able to do this, we had to switch our language in DrScheme to be R5RS. The designers of "Pretty Big" have decided to eliminate these list surgery functions from their variation of Scheme. So in the "Pretty Big" variant that we're using, normal lists are immutable, just as they are in ML. You can build up mutable lists by making calls on a function called mcons and the mutating functions are called set-mcar! and set-mcdr!.
We then looked at some examples that involve append:
(define x '(1 2 3)) (define y x) (define z '(1 2 3)) (define a '(a b)) (define b (append a x)) (define c (append a y)) (define d (append a z))Scheme, like ML, will try to avoid copying when it doesn't need to. In this case, it needs to make a copy of the first argument to append, but it doesn't need to copy the second argument. So when we asked whether any of b, c, and d were eq? to each other, the answer was no. But we found that b and c shared a substructure because the second argument to append is not copied:
> (eq? (caddr b) (caddr c)) #tI mentioned that we would have a final exam question that involves understanding this.
I then talked about how the mutating functions can be used to create a local variable that only certain functions have access to:
(define incr null) (define get null) (define m 3) (let ((n 0)) (set! incr (lambda (i) (set! n (+ n i m)))) (set! get (lambda () n)))We saw that the get function would report the value of n and the incr function was able to increment n, but we can't access n from the top-level environment:
> (get) 0 > (incr 3) > (get) 6 > (incr 5) > (get) 14 > n * reference to undefined identifier: nThe call on let introduces a local scope in which n is declared. Each lambda that appears inside the let causes Scheme to set up a closure that has a reference to that inner environment that has the variable n.
We then spent time discussing a technique known as memoization in which we remember what values were returned by various calls on a function. The idea is similar to the idea of caching. As a function is called, we keep track of what values were returned for each call, memoizing the result. This is a useful technique that can be used in any programming language. It is particularly helpful for speeding up recursive definitions that compute the same value multiple times.
In a previous lecture we wrote an inefficient version of the fib function for computing Fibonacci numbers:
(define (fib n) (if (<= n 1) 1 (+ (fib (- n 1)) (fib (- n 2)))))The complexity of this function is exponential. We were able to rewrite it using a more iterative approach, but we can use memoization instead. We set up a global variable called answers with the first two values:
(define answers '((0 . 1) (1 . 1)))Then in writing the function, we first did a lookup against our list of answers. If we've already computed that Fibonacci number, then we just return its value. Otherwise we compute an answer and store it in our list of answers so that we never compute it again:
(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.