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.
Translational Semantics in Python
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.
Python has lambda
expressions, too, so we could
even do this in a way similar to Racket's anonymous function:
(lambda x: x*something)(2)
This sort of construction does not feel as natural in Python as it does in Racket, because it is not the usual style of programming in Python. 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.
If you'd like to play with this Python code on your own, download this code file. It contains all three versions of the "compute with x" code above.
The Benefit of Translational Semantics in a Language
The syntax of a programming language generally caters to us programmers, helping us express the things we want to express in a concise and correct way. At the implementation level, though, language interpreters are less concerned with ease of programming than they are with efficiency, completeness, and correctness of execution. 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!
Practice Exercises
Both of the code files linked above are available in a zip file for your convenience.
Now for some practice...
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))