Local Variables and lambda

Local Bindings and let

In Session 16, we learned that Racket has a way of creating expressions that use local variables: the let expression. Here is a let expression that creates a local variable named x, assigns it the value 3, and uses it to compute another value:

(let ((x 3)) 
  (+ x (* x 10)))

The general form of a Racket let expression is:

<let-expression> ::= (let <binding-list> <body>)

  <binding-list> ::= ()
                   | ( <binding> . <binding-list> )

       <binding> ::= (<var> <exp>)

          <body> ::= <exp>

The let special form takes two arguments. The first is a list of variable bindings. Though Racket permits an empty list, in practice we almost never use one. (In all my years of programming in Lisp, Scheme, and Racket, I have never written a no-variable let expression.) The second is an expression that uses these bindings.

Quick Exercise: Why does let have to be a special form? It cannot evaluate the binding list as a regular Racket expression: it is a list of variable/value pairs. It does have to evaluate all of the value expressions in the usual way, though.

Let's look a bit closer at how the let expression works.

First, let's introduce a new term. The region of an identifier is the code where that identifier has meaning.

The region of a local variable created using let is the body of the let expression. This is important. It means that we cannot refer to a let variable outside of the body.

Consider this example:

(+ (let ((x 3))                   ; what is the value? 
      (+ x (* x 10)))
    x)

The following expression tells us a little more about how let works:

(define x 5)

(+ (let ((x 3))                   ; what is the value now? 
      (+ x (* x 10)))
    x)

When this expression is evaluated, it returns the value 38 because:

Do you notice any similarity between this example and our recent discussion of free and bound variables? That's not accidental...

The region of a local variable being the body of the let expression also means that we cannot use one variable's value in the expression that computes another variable's value:

(let ((x 3)
      (y (* 2 x)))   ; what goes wrong here?
  (+ x (* y 10)))

We'll explore this notion further in our next session.

We can use let expressions to create local variables for all the same reasons we use local variables in other languages. As we saw in Session 16, one such use is to store the result of a computation so that we can use it twice.

Consider this helper function, which you wrote for positions-of on Homework 4:

(define positions-of-helper
  (lambda (s los position)
    (if (null? los)
        '()
        (if (eq? s (first los))
            (cons position
                  (positions-of-helper s (rest los) (add1 position)))
            (positions-of-helper s (rest los) (add1 position))))))

In the case of a pair, we always need to know the positions of s in the rest of the list. This code repeats the call, resulting in code that is harder to read. We could use a local variable to hold the result for the rest of the list:

(define positions-of-helper
  (lambda (s los position)
    (if (null? los)
        '()
        (let ((positions-for-rest
                (positions-of-helper s (rest los) (add1 position))))
          (if (eq? s (first los))
              (cons position positions-for-rest)
              positions-for-rest)))))

In some situations, we might prefer this solution.

If you'd like to play with this code on your own, download this code file. It contains both versions of positions-of, as well as code from earlier in this reading.

Translational Semantics

Semantics refers to what a programming language construct means. Consider the let special form we have just discussed. How are we to interpret a let expression? This is important for human readers as well as language processors such as our Racket interpreter and our C++ compiler.

There are a number of ways to describe the semantics of a programming language feature. For instance, we could write a definition in English or some other natural language. But such definitions tend to be imprecise or ambiguous, even for human readers.

One of the more natural ways for a computer scientist to describe the semantics of a language feature is to write a program. We can translate expressions that use one feature into expressions that use another feature, perhaps one that we already understand well. This is called a translational semantics. Let's take a look at a translational semantics for the let expression.

A Translational Semantics for let

The primary purpose of a let expression is to bind variables to values. We know, too, that the application of a lambda expression binds variables to values, for use in evaluating an expression that contains those variables.

Recall that a let expression has the following form:

(let ((<var_1> <exp_1>)
      (<var_2> <exp_2>)
            .
            .
            .
      (<var_n> <exp_n>))
  <body>)

The semantics of this expression bind the value of <exp_i> to <var_i> in <body>. As noted above, the variables are bound to their values only in the body of the let expression. This is called the region of the variables. lambda expressions work the same way: formal parameters are bound to their values only in the body of the lambda expression.

So, we can express the meaning of a let expression using the following lambda expression:

((lambda (<var_1> <var_2>...<var_n>)
    <body>)
  <exp_1> <exp_2>... <exp_n>)

In fact, many Scheme interpreters automatically translate a let expression into an equivalent lambda application whenever they see one!

We programmers can do the same thing. Just as we could use a while loop to write Java code without for loops, we can write code using lambda application to write Racket code without let expressions. In both cases, though, the code we produced would probably not be as nice to read, and it would take more effort to write.

The idea of a treating a local binding as a syntactic abstraction is not unique to Racket. We can apply the same semantics to local variables in languages like Python or Java.

Consider the following snippet of Python:

x = 2
# ... do some stuff
return x * something

This code creates a local variable, x, assigns it the value 2, and uses the variable to compute a result. We can write code that has exactly the same meaning and no local variable by creating and calling a new helper function:

return helper(2)

def helper(x):
    # ... do some stuff
    return x * something

This code creates a formal parameter, x, assigns it a value when we call the function, and uses the variable to compute a result — with no no local variable.

Of course, this sort of construction is not as natural in Python as it is in Racket, because it has harder to create and use Python functions "on the fly". But we programmers do this sort of thing all the time, whenever we recognize a need to factor out functionality for reuse in a function.

Note: Study this Python example. It is often a stumbling block for students.

The syntax of a programming language generally caters to us programmers. At the implementation level, though, language interpreters are less concerned with ease of programming than they are with efficiency, completeness, and correctness of translation. And that is the way we programmers like it!

A language interpreter can accommodate both sides of this equation. Racket allows us to program with the syntactic sugar of let but pre-processes it away before evaluating our program. A Racket compiler can translate any let expression into an equivalent lambda application before evaluating it.

Don't make the mistake of thinking that the idea of pre-processing syntactic abstractions away is unique to Racket or to odd functional programming languages. C++ was designed as a language made up almost entirely of syntactic sugar! Its abstractions (classes and members) can be — and originally were — pre-processed into C code with structs that is suitable for a vanilla C compiler.

The key point to note here is this:

Local variables are not essential to a programming language!

Quick Exercise: For some practice, try converting these let expressions into equivalent lambda expressions:
(let ((x 5)        ;; Exercise 1
      (y 6)
      (z 7))
  (- (+ x z) y))

(let ((x 13)       ;; Exercise 2
      (y (+ y x))
      (z x))
  (- (+ x z) y))