and mutually recursive, parts.
For the last few sessions, we have been discussing different techniques for writing recursive programs, all based on the fundamental technique of structural recursion. Last time, we introduced a new topic in the study of programming languages: the static properties of variables. That included the definition of a little programming language that will serve as our testbed for studying the topic.
[ We review that reading in class ].
Today, we use our techniques for writing recursive programs to write a program to process programs in our little language. Our task is straightforward:
Does a variable occur free or bound in a given piece of code?
When we write programs to process other programs, we see quickly why knowing how to write recursive programs is so important: Programming language specifications are almost always highly inductive!
As we learned last time, if a program feature is static, then its value can be determined by looking at the text of a program. A person can look at the code, of course, but what about another program? The text of a program is data, so we ought to be able to give the text as input to another program that determines the value of a static feature. This is just what compilers, type checkers, IDEs, and all sorts of other programming tools do: examine a program to extract its static features.
Let's write our second program that takes another program as input. [ Second? ]. The input program will be written in the little language we saw last time:
<exp> ::= <varref> | (lambda (<var>) <exp>) | (<exp> <exp>)
Let's define a function named (occurs-bound? var exp), which answers this question:
Does a given variable reference var occur bound in an expression exp from our little language?
Writing this function will help us in at least three ways:
Last time, we learned the terms occurs bound and occurs free. A variable "is bound" or "occurs bound" in an expression from our little language if it refers to the formal parameter in the expression. A variable reference "is free" or "occurs free" in an expression if it is not bound.
To write code that implements these definitions, though, we need more formal definitions of occurs free and occurs bound. Because our language definition is inductive, we can give these terms inductive definitions, too.
A variable v occurs bound in an expression exp if and only if:
By definition, no variable occurs bound in an expression that consists of a single variable reference.
- exp is of the form (lambda (var) body) and either
- v occurs bound in body, or
- v occurs free in body and v is the same as var.
- exp is of the form (exp1 exp2) and v occurs bound in either exp1 or exp2.
A variable v occurs free in an expression exp if and only if:
- exp is a variable reference and is the same as v
- exp is of the form (lambda (var) body), v is different from var, and v occurs free in body.
- exp is of the form (exp1 exp2) and v occurs free in either exp1 or exp2.
With these definitions, we are ready to write our function.
But wait... We will be manipulating a data structure that uses Racket lists to represent two different kinds of expression. Some lists denote lambda expressions. Other lists denote applications of lambda expressions to arguments.
This will require us to use many cars and cdrs to access parts of the data. What's worse, they will mean different things in the different parts of the same procedure!
This data type begs us to use the Syntax Procedures design pattern. I ask you to read about this pattern for next time. For now, we will see it in action on our problem.
Before we begin to implement our solution, I have created these syntax procedures for our little language. There are two kinds of syntax procedure in the file:
We are used to Racket data types having type predicates -- for example, symbol? and number?. We have also seen that Racket data structures have access procedures -- for example, car, rest, and vector-ref. I have simply defined analogous functions for our data type, the syntax of the little language.
These procedures allow us to write occurs-bound? in terms of the little language, rather than in terms of Racket's cars and cdrs, firsts and rests. It lets us think only about the problem spec and the language, not the underlying implementation. The difference will be noticeable.
Finally, we are ready to begin writing occurs-bound?.
As always, we base our procedure on the inductive definition of the data type it manipulates. An expression in the language can be one of three alternatives. Following the Structural Recursion pattern, our procedure will make a three-way choice, with one arm in the procedure for each arm in the definition.
Let's use a cond expression instead of an if, to simplify the structure of our code:
(define occurs-bound? (lambda (s exp) (cond ((varref? exp) ;; handle a variable reference ) ((app? exp) ;; handle an application ) (else ;; handle a lambda expression ) )))
I swapped the order for handling applications and lambdas because the definition of "occurs bound?" is simpler in the application case than in the lambda case. Putting default cases and other simple cases at the top of a procedure makes it easier to read. I also like it because it encourages me solve the easier cases first.
Handling variable references is easy. Our definition says, No variable occurs bound in an expression consisting of a single variable reference, so:
(define occurs-bound? (lambda (s exp) (cond ((varref? exp) #f) ((app? exp) ;; handle an application ) (else ;; handle a lambda expression ) )))
How can a variable occur bound in a procedure application? The application itself doesn't bind a variable. It is simply a list of two expressions. So s can occur bound in an application only if it occurs bound either in the procedure expression or in the argument expression:
(define occurs-bound? (lambda (s exp) (cond ((varref? exp) #f) ((app? exp) (or (occurs-bound? s (app->proc exp)) (occurs-bound? s (app->arg exp))) ) (else ;; handle a lambda expression ) )))
The toughest case is the lambda expression. s can occur bound in a lambda in two different ways. s can occur bound within the body of the lambda OR it can occur free in the body and be the same as the formal parameter of the lambda expression.
(define occurs-bound? (lambda (s exp) (cond ((varref? exp) #f) ((app? exp) (or (occurs-bound? s (app->proc exp)) (occurs-bound? s (app->arg exp)))) (else ; lambda (or (occurs-bound? s (lambda->body exp)))) (and (eq? s (lambda->param exp)) (occurs-free? s (lambda->body exp))) )))
Notice that the definition of occurs-bound? calls occurs-free?. This is another example of mutually recursive functions. Here, the mutual recursion results not from two data definitions that are mutually inductive, but because the definitions for the two terms are themselves mutually inductive!
In order to test this solution, we need to define occurs-free?, too. I've done that for you, with the procedure given in the code download for today. However, try to write occurs-free? on your own first before you read it. Doing so will give you some practice doing what we have just done. Then look at my solution, compare them, and make sure you understand any differences.
Notice how the use of structural recursion made this code relatively easy to write. It told us which cases to consider and, when we are considering each, we don't have to think about the other two cases at all.
Notice how the use of syntax procedures makes this code relatively easy to read and as simple as possible to write. They enable us to program using the same terms that are used in the definitions. While writing the function, we had to think only of the definition of bound variables; we didn't have to worry about which sequence of cars and cdrs to use in order to manipulate the underlying list implementation. Furthermore, if we decide to change the underlying representation of programs to some other data structure, we won't need to modify this code at all. We will need only to write syntax procedures for the new representation.
Now that the procedure is written, we can read it in much the same way as we read the prose definition of occurs bound?. Understanding the code requires as little reliance on the syntax of Racket lists as possible, because it follows our language grammar and the definition of our terms to the tee. This is an example of how using a program to describe a concept can be just as clear as a prose definition, if not clearer. And, because it is executable, we can verify that it is unambiguous.
Today's zip file includes source code for occurs-bound? and occurs-free?. Play with these functions, both to be sure you understand how to write such code and also to be sure you understand the ideas of free and bound variables. For example...
An Exercise for Home: Are occurs-free? and occurs-bound? inverses of one another? That is, in a single expression, must a variable occur either free or bound, but not both? Why or why not?
I said last time that we cannot evaluate an expression containing a completely free variable, because at run-time, the variable needs to have a binding. Such a free variable needs to be bound within an enclosing expression or at the "top-level". Racket primitives are like that. Symbols such as car and + are free in our expressions, but they are bound to their primitive values at the top level of the REPL.
(By the way... How do you think that works?)
Technically, my statement is not quite true. We can evaluate an expression that contains a free variable -- as long as the variable is never evaluated. How could that happen?
Here are two trivial examples:
> (if (zero? 0) 1 foo) 1 > (and #f foo) #f
foo is unbound, but it will never be evaluated. The value of this if expression is always 1, and the evaluation rule for the special form if never evaluates foo. This works even when foo is not bound at the top level.
The rest of this section for home study. It is also optional.
Let's try a bit of Racket mental gymnastics. Can you create an expression that
We could try a lambda expression:
(lambda (x) y)
y is unbound--but if no one ever applies the lambda expression to an argument, then y will never be evaluated!
But if someone does apply the lambda to an argument, the interpreter will evaluate the y. So the value of applying the lambda depends on the value of y. That's another wrinkle. Can we iron it out?
Here is a hint: Suppose I have a function that makes no use of its formal parameter. That is, its value is independent of the value of any argument that is passed to it. Here is an example:
(lambda (x) (lambda (y) y))
This function takes some value, binds it to x, and then ignores it, returning a lambda expression that doesn't refer to it.
Can you use this idea to create an expression that contains a free variable and whose value doesn't depend on the value of that variable? How about this example?
( (lambda (x) (lambda (y) y)) x )
The value of this expression doesn't depend on the value of x! Alas, unlike the if expression and first lambda expression above, the Racket interpreter will evaluate the x -- in order to pass it as an argument. This does, however, show us that the value of an expression can be independent of the value of a free variable it contains. That is a step forward.
Now we know what we have to do: write a function that
If we pass a lambda expression that contains a free variable, the variable won't be evaluated until the lambda is applied. But if the receiving function never uses its argument, the free variable will never be evaluated!
> ( (lambda (x) (lambda (z) z)) (lambda (x) y) ) #<procedure>
The y in the procedure passed as an argument is free, but never evaluated. We can apply this function to any argument:
> (((lambda (x) (lambda (z) z)) (lambda (x) foo)) 'x) 'x
Yes, this is only an academic exercise. You aren't likely ever to need such a function, certainly not in this course. Dr. Racket won't even let you do it in source code! But it's a useful little puzzle to help us explore and understand better the idea of free and bound variables. (Sometimes, computer scientists like to play fun little games that other people might not see as fun! I hope you at least find it instructive.)