Session 16
Syntactic Abstraction: Local Variables
For Want of a Local Variable...
Racket has a primitive function named assoc. It
returns the first pair in a list that starts with a given
symbol:
> (assoc 'c '((a . 11) (b . 24) (c . 3))) '(c . 3)
If it doesn't find such a pair, it returns false:
> (assoc 'd '((a . 11) (b . 24) (c . 3))) #f
This two-pronged feature — able to return a match on
success and able to return false on a failure — makes
assoc handy for implementing
the lookup function
that I needed for
the second version
of my cipher language evaluator:
(define (lookup var env)
(if (assoc var env)
(cdr (assoc var env))
(error 'lookup "invalid variable reference ~a" var)))
... which gives us the behavior we need:
> (lookup 'c '((a . 11) (b . 24) (c . 3)))
3
> (lookup 'd '((a . 11) (b . 24) (c . 3)))
lookup: invalid variable reference d
This code is slick, but we have to call assoc
twice whenever we find the var we are looking
for: first to see if there is a match, and second to pull
the answer out of the pair. Can't we do better? Sure: we
can use a local variable.
"Um, Professor Wallingford. You have not shown us how to use local variables in Racket yet.
This has caused some of you extra friction as you've learned to write Racket functions. That was intentional, but not to be mean. We were learning a new style of programming, and local variables make it too tempting to return to our old style of programming.
You see, one of the main reasons we use local variables in other languages is to sequence a computation: first do this, then do this, ..., with local variables holding partial results along the way. But that's not the way we think in functional programming, which uses functions to do as much work as possible.
So we forewent local variables for a while so that you would have a chance to practice the new style without so much temptation to fall back into procedural thinking. (You may have been tempted, but we didn't have the tool...) I was careful to select as many problems as possible whose solutions did not need local variable, so that we could practice with as few distractions as possible.
Writing lookup reminds us that we use local
variables for other reasons, too. We use them not only to
sequence a computation but we also use them to
give a name to a value for readability and to
cache a value for reuse.
In lookup, I want to call assoc
once and give a name to its value so that I can use it twice.
If I create a local variable named match:
match = (assoc var env)
then I can say:
(if match
(cdr match)
(error 'lookup "invalid variable reference ~a" var))
The functional programmer in me knows that I can already do this, using a function:
(define get-value
(lambda (match)
(if match
(cdr match)
(error 'lookup "invalid variable reference"))))
That creates a name and uses the named object twice. If I call it:
(get-value (assoc var env))
I assign a value to match and get a result.
Voilá! That's the body of lookup:
(define (lookup var env) (get-value (assoc var env)))
I can even do this without creating a stand-alone function, if
my lambda-fu is strong, or if I remember the idea
of
program derivation:
(define (lookup var env)
((lambda (match) ; now var is available again!
(if match
(cdr match)
(error 'lookup "invalid variable reference ~a" var)))
(assoc var env)))
Success.
"That's great and all, Professor Wallingford, but why do I
have to think about lambda here? Wouldn't it
be nice just to use a local variable?""
Yes, yes, it would. The designers of Racket give us the ability to do so. But we just saw something very important:
A programming language doesn't need new machinery
under the hood to make local variables work.
A language that supports function calls
has everything it needs to get the job done.
Syntactic Abstraction
We are all familiar with programming language features that are
not strictly necessary to make the language complete. A good
example is the for statement in Java.
for is not strictly necessary, because we can
always replace a for loop with a
while loop that does the same thing.
for is nice, though, because it brings all of the
control elements of the loop together into one place.
Or consider the simple assignment statement:
x = y + z
Programmers often find themselves using this statement to update the value of a variable:
x = x + 5 x = x + 10 x = x + 100 ...
... which gives rise to the convenient shorthand we see in Python and Java:
x += 5
In languages like C++ and Java, programmers write lots of
for-loops and find themselves incrementing lots
of counters:
x = x + 1 x = x + 1 x = x + 1 ...
... which gives rise to the even shorter shorthand we see in Java and C and C++:
x++
We don't need these extra constructs, but they sure are handy.
The formal name for such language features is syntactic abstraction, though many people call them syntactic sugar. They make programming easier by abstracting away the details of a common construction into a simpler or more direct statement. They are convenient but not necessary. They make the language sweeter for humans.
As programmers, we often feel as if syntactic abstractions are
essential to our task of writing code easily. Indeed, much of
my research over many years in artificial intelligence and
object-oriented programming was built on the foundation of
creating and using very high-level programming languages for
developing intelligent systems. These languages aren't
necessary. People could always have written their programs in
Python, Ada, Java, Racket, Ruby, Smalltalk, Lisp, or C++. But
the new languages made it possible to write programs in terms
of domain knowledge and problem-solving strategies, rather
programs in terms of for statements, or in terms
of car and cdr expressions.
But from the programming language perspective, syntactic sugar is not essential and complicates the process of interpreting programs written in the a language. Part of our study of the design of programming languages is to identify which features are essential so that we can understand how interpreters work, and how an interpreter can pre-process the sugar away.
We have already learned that one feature we probably thought was essential — functions that take more than one argument &mdash is really syntactic sugar. The idea underlying this abstraction was called currying.
We have also used Racket's multi-way conditional expression,
cond, and learned
that it is a syntactic abstraction of the more basic
if expression. (Or vice versa!)
Over the next few sessions, we will consider a number of other common programming language features and investigate whether they are essential or are "just sugar".
Local Bindings and lambda
Up to this point in the course, we have used only three kinds of identifiers in our programs:
- the names of primitive functions,
- the names of functions and other data values that we defined at the top level, and
- the names of formal parameters on functions.
Of these, only the formal parameters behave like the "local variables" that we are accustomed to using in other programming languages.
These names are local to the function in which they are declared. They have not yet been variable, because we have not had a way to assign new values to a name yet. But we haven't needed to. Any time we have needed to "change the value" of an variable, we have passed the new value as an argument in a recursive call.
This idea also explains how we have managed to write programs
for eight weeks without creating any local variables. Any
time we needed a local variable, like position
in the function
positions-of,
we created a new function with a new parameter:
(define positions-of-helper
(lambda (s los position)
...))
and passed the value of the variable when we called the function:
(define positions-of
(lambda (s los)
(positions-of-helper s los 0)))
A function satisfied our needs.
The lambda special form provides a binding
mechanism by which names are created and values are associated
with names.
Last week, we considered to determine statically whether a variable reference is bound to the value of a formal parameter in a program. Let's now move on to consider identifiers in more detail and how they get their values.
Local Bindings and let
Unsurprisingly, Racket has a way of creating expressions that
use local variables: the special form let.
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)))
We can also create and use two local variables in the same expression:
(let ((x 3)
(y 5))
(+ x (* y 10)))
Now we now have the tool we need to implement the
lookup function in the way we'd like:
(define (lookup var env)
(let ((match (assoc var env)))
(if match
(cdr match)
(error 'lookup "invalid variable reference ~a" var))))
Of course, let uses prefix notation, like the rest
of Racket, and the placement of the parentheses is —
as always!
— important. So let's have a closer look.
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.
+
The second is an expression that uses these bindings.
In all my years of programming in Lisp, Scheme, and Racket, I
have never written a no-variable let expression.
We will examine let expressions more closely in
our next session. We will also begin to use them in our own
code whenever they are helpful.
Keep in mind something we saw earlier in the session:
We were able to accomplish the same behavior without
a let expression!
Racket can take advantage of this fact!
Wrap Up
-
Reading
- Study these notes, paying attention to any ideas we did not discuss in class.
-
Read
this short section
before our next session. It looks at
letexpressions a bit more and connects them to the idea of syntactic abstraction. - Optional readings — If you would like to see more examples involving local variables, read Sections 2.4 Variables and Let Expressions and 2.5 Lambda Expressions in Dybvig's online The Scheme Programming Language. You can stop when you reach Section 2.6.
-
Homework
- Homework 7 will be available after next session [after spring break].
-
Quiz
- Quiz 2, over recursive programming techniques, is today at the end of class.