Denoted Values versus Expressed Values:
Name, Value, and Location
This reading also contains a bonus practice problem. Read to the end!
Denoted Values versus Expressed Values
Most programming languages support two kinds of value:
- the set of values that can be named with a variable or a formal parameter. These are called the denoted values of the language.
- the set of values that can be "returned" as an expression. These are called the expressed values of the language.
One of the things that distinguishes Racket from most languages you knew before is that functions are both denoted and expressed values.
Why does introducing variable assignment into our language require that we reconsider this topic? Couldn't we just model a variable assignment as a new addition to the environment?
A few problems would result from such an approach. On the practical side, this approach leads to environments that grow larger with each assignment statement. This makes our interpreter waste space. A loop that reassigned a variable 100 times would add 100 new bindings to the environment.
This approach also makes it difficult to determine whether a variable exists yet, and that makes it difficult for the language and interpreter to help users. Do we want to allow assignment statements to create new variables on the fly? Or should the assignment proceed only if the variable already exists?
And what if we decide to make our language statically typed? A new problem arises: how do we know if the value being assigned is legal for variables of that type?
All these problems arise because we have sidestepped the distinction between a variable's value and its location:
When we treat a variable as a value in an expression, we care only about its current value. Where the object resides in memory, or what it looks like, is immaterial. But when we make an assignment to the variable, we are concerned with both where the object resides in memory and what it looks like, because our goal is to change the value that is recorded there.
The list of questions goes on. We see this distinction in many different forms, including:
- pass by value and pass by reference
- values and pointers
- lvals and rvals in the C language
- objects and references in the Java language
The distinction between "variable as value" and "variable as location that holds a value" is one that our interpreter should model as explicitly as possible.
The solution is to create a level of indirection between variables and their values. This intermediate level of representation corresponds to the storage aspect of the object. When we wish to treat a variable as a value, we go through this level to reach the value, When we wish to treat a variable as a location, as in an assignment, we work at the intermediate level.
For an upcoming assignment, you will first implement an ADT that
supports this kind of reasoning: the cell
. Then, you
will modify your language interpreter so that its environment holds
variable/cell pairs, rather than variable/value pairs.
With each variable associated with a cell, we will be able to refer to the value that a cell holds (when looking up the value of a variable reference) or change the value stored in the cell. Thus, cells directly support the idea of mutable data.
On the current assignment, the set of expressed values in your language (colors) is identical to the set of denoted values (colors). That is, the value of every expression is a color, and the value of every variable was a color.
When we add mutable data to the language, we will make a subtle change to the language's semantics. At that point, the expressed values in the language will still be colors, but the denoted values will be cells. Whenever we wish to determine the actual value of a variable, we "dereference" the cell, much we follow a pointer to the location of the desired value.
A Bonus Exercise, with "I" and "O"
Racket has more than output operators. It also supports input
with several different functions. The reading for today mentioned
read
,
which reads an object, parses it, and returns it as a typed Racket
value.
The reading on imperative programming in Racket explored some of Racket's input operators. The zip file for this reading includes a file demonstrating some of them.
Let's write a function that takes input from the user.
I have written an interactive Intro to Computing-like program using Racket's I/O operators:
(define right-triangle (lambda () (let ((x (read-num "One side of triangle:")) (y (read-num "The other side:"))) (displayln "The hypotenuse is " (hypotenuse x y)))))
This function requires a specialized input function that displays a prompt to the user and rejects bad values. So...
(read-num prompt)
, which displays
the prompt and reads a value from the user. If the value is a
number, read-num
returns it. If not, it displays an
error message and tries again. For example:
> (right-triangle) One side of triangle: e ; read x Illegal value ; ... One side of triangle: 3 ; ... The other side: 4 ; read y The hypotenuse is 5 >
You may want to use begin
for this exercise, in
addition to read
and several things you already know,
such as let
and recursion.
Solutions to the Exercise
Check out
this solution,
which uses a let
expression to cache the value entered
by the user so that it can be tested and returned.
(define read-num (lambda (prompt) (display prompt) (display " ") (let ((answer (read))) (if (number? answer) answer (begin (displayln "Illegal value") (read-num prompt))))))
I need begin
here, because the else clause has to
display an error message before making its recursive call. Notice
that this function is tail recursive... It will not create new
stack frames. read-num
behaves like a loop.
read-num
is a useful function, but it returns only
numbers. What if we want prompted input for other types? I will
have to write a nearly identical function, changing only the type
predicate.
Higher-order functions to the rescue!
We can factor out the type-check and use this function as a
template to generate "prompted read" functions for any given type
of input. read-type
uses letrec
to
create and return a recursive function. (We saw an example like
this in Session 23,
taking-using
. What would happen if we didn't use
letrec
here?)
Finally, notice the style of code in this file. We still use functions to do computations. The interactive program and I/O functions keep computation separate from interactions with the world. This will allow us to do I/O and still benefit from the advantages of functional programming. (Some functional programmers call this the "10% Observation".)
Download this file for all of the code from this reading.