Session 13
An Application of Structural Recursion: Variable Binding

Quick Review of Homework 5

Problems 2, 3, and 4 create operations for X-lists that we could implement over flat lists using map and apply. The mutual recursion pattern makes them almost as easy to implement over the nested lists.

Implement one or two — total-length.

The solution to prefix->infix is so very simple. Follow the data structure...

Congratulations! You have written your first translator. This function converts a legal program in one form into a legal program in another form. At its heart, this is what a compiler does. The output of prefix->infix is a legal Python program. We can use the same idea to write another prefix expression translator that translates Racket expressions into legal programs in languages like Forth and Joy. (We will see Joy again later in the semester).

A four-panel comic strip. The first panel shows a flyer hanger on a tree. The flyer says 'Problems with Recursion?' and has a bunch of tabs at the bottom for people to tear off. In panel two, a man tears one off. In panel 3 he expresses surprise and exclaims 'What the-'. In panel 4 we see the tab in his hand. It is a miniature copy of the flyer.
Programs are built out of recursive parts.

Where Are We?

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.

Review key ideas from that discussion:
  • static and dynamic properties
  • a little language to use as a testbed
  • bound and free variables

Some Racket functions that you may think of as having only bound variables also contain free variables.

Consider this Racket function. It is not a combinator.

(define (sum-of-squares x y)
  (+ (* x x) (* y y)))

(define sum-of-squares
  (lambda (x y)
    (+ (* x x) (* y y))))

Why? Because + is a free variable! You can verify this for yourself by capturing the variable reference in a lambda:

(define capture+
  (lambda (+)
    (lambda (x y)
      (+ (* x x) (* y y)))))

What is the value of ((capture+ -) 3 4)?

Understanding the idea of bound and free variables — a static feature of Racket programs — helps us understand why Racket operators work as they do.

A Static Analysis Problem: Bound Variables

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.

Today, we use our techniques for writing recursive programs to write a program that processes programs in our little language. Our task is straightforward:

Does a variable occur bound in a given piece of code?

Our function will take a program as input. The input program will be written in the little language we saw last time:

<exp> ::= <varref>
        | (lambda (<var>) <exp>)
        | (<exp> <exp>)

Let's formalize our task so that we know our goal more clearly:

Define a function named (occurs-bound? v exp) that answers this question:

Does a given variable reference var occur bound in expression exp from the little language?

Writing this function will help us in at least three ways:

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!

Before writing our function, let's give ourselves a couple of useful tools.

Formal Definitions of Free and Bound Variables

Last time, we learned the terms occurs bound and occurs free. A variable "is bound" or "occurs bound" in an expression if it refers to the formal parameter in an expression that contains it. A variable reference "is free" or "occurs free" in an expression if it occurs but 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.

First, occurs bound:

A variable v occurs bound in an expression exp if and only if:
  • 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.
By definition, no variable occurs bound in an expression that consists of a single variable reference.

Now, occurs free:

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 better prepared to write Racket code that implements our function.

Syntax Procedures for the Little Language

But wait... How will we write code to determine if, say:

exp is of the form (lambda (var) body)

is true?

We could implement a function to verify that exp is a list of size three, whose first item is the symbol lambda, whose second item is a list of one symbol, and whose third item is a legal expression in the language. Such code will obscure our definition of what it means to "occur bound" and make it much harder to read!

Indeed, we will be using Racket lists to represent two different kinds of expression. Some lists denote lambda expressions. Other lists denote applications of functions to arguments.

This will require us to use many cars and cdrs, or firsts and rests, or seconds and thirds to access parts of the data. What's worse, they will mean different things in the different parts of the same function!

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 three kinds of syntax procedure in the file:

Demonstrate the syntax procedures.

These functions are not as unusual as they might at first seem.

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 in the code we write will be noticeable.

Implementing occurs-bound?

Finally, we are ready to begin writing occurs-bound?.

As always, we base our function 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 function will make a three-way choice, with one arm in the function for each arm in the definition.

Let's add a fourth case, error, to handle invalid expressions...

We can use a cond expression instead of an if, to simplify the layout of our code:

(define (occurs-bound? s exp)
  (cond ((varref? exp) ;; handle a variable reference )
        ((app? exp)    ;; handle an application       )
        ((lambda? exp) ;; handle a lambda expression  )
        (else (error 'occurs-bound? "invalid expression ~a" exp))  ))

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 function makes it easier to read. I also like doing this 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? s exp)
  (cond ((varref? exp)
            #f)
        ((app? exp)    ;; handle an application       )
        ((lambda? exp) ;; handle a lambda expression  )
        (else (error 'occurs-bound? "invalid expression ~a" exp))  ))

How can a variable occur bound in a function 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 function expression or in the argument expression:

(define (occurs-bound? s exp)
  (cond ((varref? exp)
            #f)
        ((app? exp)
            (or (occurs-bound? s (app->proc exp))
                (occurs-bound? s (app->arg  exp))) )
        ((lambda? exp) ;; handle a lambda expression  )
        (else (error 'occurs-bound? "invalid expression ~a" exp))  ))

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? s exp)
  (cond ((varref? exp)
            #f)
        ((app? exp)
            (or (occurs-bound? s (app->proc exp))
                (occurs-bound? s (app->arg  exp))) )
        ((lambda? exp)
            (or (occurs-bound? s (lambda->body exp))
                (and (eq? s (lambda->param exp))
                     (occurs-free? s (lambda->body exp)))))
        (else (error 'occurs-bound? "invalid expression ~a" exp))  ))

Notice that the definition of occurs-bound? calls occurs-free?. This is another example of mutually recursive functions. Here, though, 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 and included the function in the code download for today. However, try to write occurs-free? on your own first before you read it:

Define a function named (occurs-free? v exp) that answers this question:

Does a given variable reference var occur free in expression exp from the little language?

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.

There are several things to notice about occurs-bound?:

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

A Study Question for Quiz 2: Are occurs-free? and occurs-bound? inverses of one another? That is, for a single expression exp:
  • If (occurs-bound? exp) is true, then (occurs-free? exp) is always false.
  • If (occurs-bound? exp) is false, then (occurs-free? exp) is always true.
Why or why not?

Unbound Variables

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.

Let's try a bit of Racket mental gymnastics. Can you create an expression that:

  1. doesn't use a conditional,
  2. contains an unbound variable, and
  3. whose value is not affected by the value of the unbound variable?

Hint: how can lambda help?

If you are curious, check out this short reading that works through one idea.

Wrap Up