CS 536: Programming Language Concepts
Notes on Tail Calls

This page summarizes CPS and gives an introduction to continuations.


Introduction to Tail Calls

A function call is a tail-call (or, is in tail-position) in an expression if the value returned from the call is the value of the entire expression. Examples:


Why Do Tail Calls Matter?

Tail calls matter to us for two reasons:

  1. When we discussed the web assignment, we said that the difference between the textual I/O version and the web/script version was that the textual version was "tree-like" (see the calls to prompt-read-many nested inside the calls to display-order), while the script version was "linear" (each script function ended with a function call). Tail-calls distinguish "tree-like" from "linear" programs. In a linear program (like web scripts) all calls to user-defined functions must be tail calls.

    Thus, to compile tree-like programs into linear programs, we need to be able to identify and remove non-tail calls.

  2. Tail-calls also use stack space more efficiently. To see this, consider the following two versions of sum (over a list of numbers):

    (define (sum alon)
      (cond [(empty? alon) 0]
            [(cons? alon) (+ (first alon) (sum (rest alon)))]))
    
    (define (sum2 alon)
      (sum-help alon 0))
    
    (define (sum-help alon total)
      (cond [(empty? alon) total]
            [(cons? alon) (sum-help (rest alon) (+ (first alon) total))]))
    

    Sum and sum2 return the same answers, but the shape of their computations is somewhat different. Consider the major steps when running each program on the input (list 1 3 5 7):

    (sum (list 1 3 5 7))
    (+ 1 (sum (list 3 5 7)))
    (+ 1 (+ 3 (sum (list 5 7))))
    (+ 1 (+ 3 (+ 5 (sum (list 7)))))
    (+ 1 (+ 3 (+ 5 (+ 7 (sum empty)))))
    (+ 1 (+ 3 (+ 5 (+ 7 0))))
    (+ 1 (+ 3 (+ 5 7)))
    (+ 1 (+ 3 12))
    (+ 1 15)
    16
    
    (sum2 (list 1 3 5 7))
    (sum-help (list 1 3 5 7) 0)
    (sum-help (list 3 5 7) 1)
    (sum-help (list 5 7) 4)
    (sum-help (list 7) 9)
    (sum-help empty 16)
    16
    

    Notice how the sum version builds up a list of pending computations waiting for the successive results of sum, while no computations are waiting for the results of the calls to sum-help. In other words, the calls to sum build up in an arrow-like shape; the sum-help calls, in contrast, all line-up underneath each other).

    Pending computations have to be stored somewhere (they are stored on the stack). Thus, the sum version uses stack storage (plus some extra time to manage the stack storage) that the sum-help version doesn't need. If we ran these two programs on a long-enough list, we could run out of memory using the sum version, while not running out of memory with the sum-help version.

So, we should care about tail-calls for two reasons: they capture the structure of web programs (we can't write scripts without them) and they can lead to space efficiency if a language exploits them.


How Do We Turn Non-Tail Calls into Tail Calls?

Fortunately, we can convert non-tail calls into tail calls through a fairly straightforward method. We'll demo the method first, then summarize the steps. Note that this method preserves the behavior of the original program (as well it should!).

Consider the following definition and expression:

(define (f x) (+ x 3))
(* (f 5) 2)

The call (f 5) is not a tail-call. If we wanted it to be a tail-call, we'd have to move it outside of the multiplication. Doing so, though, would leave a hole in the multiplication expression, as follows:

(* hole 2)

Having the unbound variable "hole" in the expression is bad though, since unbound variables lead to errors. We can fix this problem by adding (lambda (hole) ...) to the expression:

(lambda (hole) (* hole 2))

Now return to the call to f that we yanked out. To put the call in tail position, we can't embed it in another expression. So, the call to f must be at the top level:

(f 5)

This isn't quite right though, because this expression would return 8 where the original expression returned 16. We need to send the return value from this call to our new (lambda (hole) (* hole 2)) expression, but while leaving the call to f at the top level. We can do this by passing the lambda expression to f as an additional argument and having f send its result to the lambda expression, as follows. To avoid confusion, I'll rename f to f/k.

(define (f/k x k) (k (+ x 3)))
(f/k 5 (lambda (hole) (* hole 2)))

The new call to f/k returns the same answer as the original call to f, but note that the call to f/k is in tail-position, while the call to f is not.

This new (lambda (hole) ...) expression captures the pending computation for the call to f. Pending computation functions are called continuations (because they show how to "continue" the computation). Conventionally, texts that teach you to pass continuations as arguments use "k" for the continuation argument. It's just a convention though (and one you can't always follow, because you can have multiple continuations in the same scope).

Summary of the method

To convert a non-tail call to function f in expression E to a tail-call:

Example: (define (f x) (+ x 3))
         (* (f 5) 2)
  1. Copy the original call to f to the top-level (ie, not nested inside any other expression)

    (f 5)
  2. Replace the entire call to f in E with a new variable.

    (* hole 2)
  3. Enclose the revised E in a lambda expression that has the new variable as a parameter. For example, assuming the variable used in the previous step was named "hole", wrap the revised E in (lambda (hole) ...).

    (lambda (hole) (* hole 2))
  4. Create a new version of f (here called f/k) that takes an additional argument (contract: a function of one argument) and calls that function on the original body of f.

    (define (f/k x k) (k (+ x 3)))
    
  5. Modify the copied call to f to call f/k, passing the (lambda (hole) ...) function as the last argument [assuming you haven't already done this -- a function never needs more than one continuation argument].

    (f/k 5 (lambda (hole) (* hole 2)))
    
  6. Make sure that all calls in the body of f/k are tail calls, and repeat this procedure as needed to convert them into tail calls.


Exercises

Try converting the following expressions using the above method:


Questions


This page maintained by Kathi Fisler
Department of Computer Science Worcester Polytechnic Institute