Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

scheme functions that "remember" values with let/set

I'm new to Scheme and trying to understand how certain values that appear within a function can persist across multiple uses. Take the following counter:

(define count
   (let ((next 0))
     (lambda ()
       (let ((v next))
         (set! next (+ next 1))
         v))))

What I can't figure out (and haven't found explained anywhere), is why next doesn't get reset to 0 every time count is used.

like image 345
planarian Avatar asked May 31 '12 17:05

planarian


People also ask

How does let work in Scheme?

In Scheme, you can use local variables pretty much the way you do in most languages. When you enter a let expression, the let variables will be bound and initialized with values. When you exit the let expression, those bindings will disappear.

What does the lambda function do in Scheme?

Lambda is the name of a special form that generates procedures. It takes some information about the function you want to create as arguments and it returns the procedure. It'll be easier to explain the details after you see an example.

How do you define a constant in a Scheme?

Just define the value at the toplevel like a regular variable and then don't change it. To help you remember, you can adopt a convention for naming these kinds of constants - I've seen books where toplevel variables are defined with *stars* around their name.

How do you call a function in another Scheme?

A function call is written as (f args) where f is the name of the function and args a space-separated sequence of arguments. So to call tab without arguments, you'd write (tab) and to call translate with the argument x , you'd write (translate x) .


3 Answers

This is called a closure. There's only one version of next in the whole program.

To make this clearer, consider the following program:

(define next 0)

(define count
  (lambda ()
    (let ((v next))
      (set! next (+ next 1))
      v))))

Now it's clear that there's only one next.

The version you wrote is different, because you've used let to make sure that only the lambda expression can see next. But there's still only one next. If you changed it to this, instead:

(define count
  (lambda ()
    (let ((next 0))
      (let ((v next))
       (set! next (+ next 1))
       v))))

Then you would create a new version of next every time, because the declaration of next is inside the lambda, which means that it happens every time that lambda is called.

like image 99
Sam Tobin-Hochstadt Avatar answered Nov 15 '22 08:11

Sam Tobin-Hochstadt


I have one thing to add to Sam's excellent answer: your question suggests that this behavior might have something to do with "let". It does not. Here's an example that does a similar thing without a "let" in it:

#lang racket

(define (make-counter-from counter)
  (lambda ()
    (set! counter (+ counter 1))
    counter))

(define count (make-counter-from 9))

(count)
(count)

The moral (if there is one): Yes! Mutation is confusing!

EDIT: Based on your comment below, it sounds like you really are looking for some insight into what kind of mental model you can use for a language with mutation.

In a language with mutation of local variables, you can't use the simple "substitution" model that replaces arguments with values. Instead, each call to a function creates a new "binding" that can later be updated (a.k.a. "mutated").

So, in my code above, calling "make-counter-from" with 9 creates a new binding that associates the "counter" variable with the value 9. This binding is then attached/substituted-for all uses of the "counter" variable in the body of the function, including those inside of the lambda. The result of the function is then a lambda (a function) that is "closed over" two references to this newly created binding. You can think of these as two references to a heap-allocated object, if you like. This means that every call to the resulting function results in two accesses to this object/heap-thing.

like image 29
John Clements Avatar answered Nov 15 '22 09:11

John Clements


I don't totally agree with your explanation. You're right in that the function's definition is evaluated only once, but the function itself is evaluated every time it is called.

The point I don't agree with is the "...rewrites the definition...", because the function is defined only once (and not explicitly overwritten).

I imagine it the following way: Due to the so-called lexical binding of variables in scheme, the scheme interpreter notices during the evaluation of the function definition that there's a variable defined in the environment of the function definition - the variable "next". Therefore it remembers not only the function definition, but the value of the variable "next", too (this means that it stores two things - the function definition and the enclosing environment). When the function in called for the first time, its definition is evaluated by the scheme interpreter within the stored environment (in which the variable "next" has the value 0, and were the value is incremented). The second time the function is called, exactly the same things happen - the same function definition is evaluated in its enclosing environment. This time, however, the environment provides the value 1 for the variable "next" and the result of the function call is 1.

To phrase is concisely: The function (definition) stays the same, it's the evaluation environment that changes.

like image 43
Valentin Huber Avatar answered Nov 15 '22 09:11

Valentin Huber