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:

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:

A graph with the node 'API' at the center. At the same level, a node labeled 'client' has a bidirectional arrow to the API node. Down a level, two nodes labeled 'implementation' have aone-directional arrow pointing to the AI node.
the difference between the value 2 and a location holding the value 2

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:

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...

Write the function (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.