5 Environmentsand Procedures
5.1 Understanding let and lambda
5.1.2.3 Procedures are Closures
Scheme procedure's aren't really just pieces of code you can execute they're closures.
A closure is a procedure that records what environment it was created in. When you call it, that environment is restored before the actual code is executed. This ensures that when a procedure executes, it sees the exact same variable bindings that were visible when it was created|it doesn't just remember variable names in its code, it remembers what storage each name referred to when it was created.
Since variable bindings are allocated on the heap, not on a stack, this allows procedures to remember binding environments even after the expressions that created those environments have been evaluated. For example, a closure created by alambdainside a letwill remember the let's
variable bindings even after we've exited the let. As long as we have a pointer to the procedure
(closure), the bindings it refers to are guaranteed to exist. (The garbage collector will not reclaim the procedure's storage, or the storage for theletbindings.)
Here's an example that may clarify this, and show one way of taking advantage of it.
Suppose we type the following expression at the Scheme prompt, to be interpreted in a top-level environment:
Scheme> (let ((count 0)) (lambda ()
(begin (set! count (+ count 1)) count)))
Evaluating this let expression rst creates a binding environment with a binding for count.
The initial value of this binding is 0. In this environment, the lambda expression creates a closure. When executed, this procedure will increment the count, and then return its value. (Note that the procedure is not executed yet, however|it's just created.) This procedure, returned by the lambda expression, is also returned as the value of the let expression, because a let returns the
value of its last body expression. The read-eval-print loop therefore prints a representation of the (anonymous) procedure.
Unfortunately, we didn't do anything with the value, like give it a name, so we can't refer to it anymore, and the garbage collector will just reclaim it. (OOPS.) Now suppose we want to do the same thing, but hold onto the closure so that we can do something with it. We'll bind a new variablemy-counter, and use the aboveletexpression to create a new environment and procedure,
just like before.
Scheme> (define my-counter (let ((count 0)) (lambda ()
(set! count (+ count 1)) count))))
my-counter
Now we have a top-level binding ofmy-counter, whose value is the procedure we created. The
procedure keeps a pointer to the environment created by the let, which in turn has a pointer to
the top-level environment, thus:
should simplify this picture and use it earlier, for the simpler example where we don't keep a pointer to the closure. Should show the envt register pointing to thelet envt at the moment the
'envt] +-->+---+---+ | | car | *--+--> ... | +---+---+ | | cons | *--+--> ... | +---+---+ | | * | | * | | * | | +---+---+ | | my-counter | *--+---+ | +---+---+ | | /|\ | | | | | 'envt] | | | +---+--+--+ | | | 'scope] | * | | | +---+---+ | | | count | *--+-->10 | | +---+---+ \|/ | /|\ 'closure] | | +---+ | +---+----* | | +---+ | | * | | +----+----+ | | | \|/ | 'code] | +---+ +---+---+ | (set! count | envt | * | | (+ count 1)) | +---+ | count | +---+
Now if we call the procedure my-counter, it will execute in its own \captured" environment
(created by the let). It will increment the binding of count in that environment, and return the
result. The environment will continue to exist as long as the procedure does, and will store the latest value until next time my-counteris called:
Scheme>(my-counter) 1 Scheme>(my-counter) 2 Scheme>(my-counter) 3
Notice that if we evaluate the let form again, we will get a new let environment, and a new
procedure that will increment and return itscountvalue|in eect, each procedure has its own little
piece of state which only it can see (because only it was created in that particular environment). If we want, we can dene a procedure that will create new environments, and new procedures that capture those environments|we can generate new counter procedures just by calling that \higher- order" procedure. (Recall that a higher-order procedure is just a procedure that manipulates other procedures. In this case, we're making a procedure that generates procedures.)
Each time make-counter is called, it will execute a let, creating an environment, and inside
that it will use lambdato create a counter procedure. Scheme> (define (make-counter)
(let ((count 0)) (lambda ()
(set! count (+ count 1)) count)))
make-counter
Each of the resulting procedures will have its own captured count variable, and keep it indepen- dently of the other procedures.
Make sure you understand that the above procedure denition could have used an explicit
lambdato create the proceduremake-counter, rather than the special procedure denition syntax: Scheme> (define make-counter
(lambda ()
(let ((count 0)) (lambda ()
(set! count (+ count 1)) count)))
You may actually nd this easier to understand, because it shows you exactly what's going on: binding make-counter and creating a procedure (with the outer lambda) that when called, will
evaluate a letto create an environment, and alambda(the inner one) to create a procedure that
captures it.)
Now we'll call the procedure created by the above denition, three times, and each time it will create a new procedure:
Scheme> (define c1 (make-counter)) C1
Scheme> (define c2 (make-counter)) C2
Scheme> (define c3 (make-counter)) C3
Now we'll call those procedures and look at their return values, to illustrate that they're inde- pendent counters: Scheme> (c1) 1 Scheme> (c1) 2 Scheme> (c2) 1 Scheme> (c2) 2 Scheme> (c1) 3 Scheme> (c1) 4 Scheme> (c3) 1
Neat, huh? The combination of block structure (local environments) with rst-class proce- dures (closures), allows us to associate state with procedures. Garbage collection makes this very convenient, because we know that the environments will hang around as long as the procedures do. If you're familiar with object-oriented programming, you may notice a resemblance between closures and \objects" in the object-oriented sense. A closure associates data with a procedure, where an object associates data with multiple procedures. After we get to object-oriented pro- gramming, we'll explain how object-oriented programming facilities can be implemented in Scheme using closures.
If you're familiar with graphical user interface systems, you may notice that GUI's often use \callbacks," which are procedures that are executed in response to user input events like button clicks and menu selections, and do something application-specic. (The application \registers" callback procedures with the GUI system, which then calls them when the user clicks on the specied buttons.) Closures make excellent GUI callback procedures, because the application can create a closure for a specic context by capturing variable bindings, to customize the behavior of the procedure.