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.
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:
-
In the expression
(+ x (* x 10))
,x
is bound to 3. -
Outside the parentheses that enclose the
let
,x
is defined to be 5.
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 struct
s
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!
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))