CPS 343/543 Lecture notes: Tail calls and continuation-passing style

Coverage: [EOPL] Chapter 7 (pp. 241-243), §8.1 (pp. 301-308), [TSPL] §§3.2 (pp. 62-70) and 3.4 (pp. 75-77), and [TSS] Chapter 19 (pp. 154-177)

Recursive control behavior

  • for instance, consider the following definition of a factorial function which naturally reflects the mathematical definition of factorial (i.e., n!=n*(n-1)!
    (define factorial
       (lambda (n)
             ((zero? n) 1)
             (else (* n (factorial (- n 1)))))))
    ;; depiction of the control context:
    (factorial 5)
    (* 5 (factorial 4))
    (* 5 (* 4 (factorial 3)))
    (* 5 (* 4 (* 3 (factorial 2))))
    (* 5 (* 4 (* 3 (* 2 (factorial 1)))))
    (* 5 (* 4 (* 3 (* 2 (* 1 (factorial 0))))))
    (* 5 (* 4 (* 3 (* 2 (* 1 1)))))
    (* 5 (* 4 (* 3 (* 2 1))))
    (* 5 (* 4 (* 3 2)))
    (* 5 (* 4 6))
    (* 5 24)
  • requires an ever-increasing amount of memory to store the control context as the depth of the recursion increases
  • question: can we define a version of factorial which does not cause the control context to grow?
  • answer: yes

Tail recursion

  • for instance,
    ;; a is called an `accumulator'
    (define factorial
       (lambda (n)
          (letrec ((fact
             (lambda (n a)
                   ((zero? n) a)
                   (else (fact (- n 1) (* n a)))))))
             (fact n 1))))
    ;; depiction of the control context:
    ;; the world is flat!
    (factorial 5)
    (fact 5 1)
    (fact 4 5)
    (fact 3 20)
    (fact 2 60)
    (fact 1 120)
    (fact 0 120)
  • when fact calls itself it does so at the tail end of the call to fact
  • such an invocation is referred to as a tail call
  • a call is a tail call if there is no promise to do anything with the returned value but return from the lambda expression
  • this version of factorial is said to use tail recursion
  • code using tail recursion only requires a bounded amount of memory to store the control context
  • contrast with first version where the recursive call is in an operand position
  • important principle: `a procedure call that does not grow control context is the same as a jump' [EOPL] p. 268

  • tail calls make recursion iterative and thus efficient
  • now the code no longer naturally reflects the mathematical definition of factorial

Continuation-passing style

  • make all recursive calls tail calls by packaging up any work remaining after the would be recursive call into an explicit continuation and passing it to the recursive call
  • make the implicit continuation capture by call/cc explicit by packaging it as an additional procedural argument passed in every call
  • this is called continuation-passing style
  • for instance,
    (define add
       (lambda (x y k)
          (k (+ x y))))
    (define multiply
       (lambda (x y k)
          (k (* x y))))
    ;; non-CPS
    (* 3 (+ 1 2))
    ;; CPS
    (add 1 2 (lambda (rtnval) (multiply 3 rtnval (lambda (x) x))))
  • re-wrote product in CPS
  • what does CPS give that call/cc does not?
    • a procedure can accept multiple continuations (e.g., success and failure continuations passed to the integer-divide procedure)
    • now the continuation can take more than one argument (because we are defining it) (e.g., the success continuation passed to integer-divide accepts two values; success was bound to list at the time of the call)
  • any program written using call/cc can be mechanically re-written in CPS without call/cc:
    ``Unfortunately, the procedures resulting from the conversion process are often difficult to understand. The argument that [first-class] continuations need not be added to the Scheme language is factually correct. It has as much validity as the statement that the names of the formal parameters can be chosen arbitrarily. And both of these arguments have the same basic flaw: the form in which a statement is written can have a major impact on how easily a person can understand the statement. While understanding that the language does not inherently need any extensions to support programming using [first-class] continuations, the Scheme community nevertheless chose to add one operation [(i.e., call/cc)] to the language to ease the chore'' [MS].
  • CPS makes recursion as efficient as iteration
  • CPS transformation, now we can have our cake and eat it too!

Full circle

    call-by-name(delay ...)(force ...)
    continuations(call/cc ...)(k ...)

    problems side-effects cause for call-by-name akin to synchronization problem for shared memory [PLPP] p. 512

Applications of continuations

  • non-local (abnormal) exits (for exceptional handling) for efficiency (i.e., to prevent returning through several layers of recursion)
  • backtracking
  • breakpoints (as used in debuggers)
  • thunks (call-by-name parameters)
  • multi-threading and multi-tasking (e.g., co-routines)
  • iterators (see [TSS] Chapter 19)
  • human-computer dialogs

End of Scheme

  • take away: exotic programming languages are powerful (we have only seen the tip of the iceberg in this class) and poorly understood
  • they are an ideal lens through which to study the core concepts of programming languages (and computer science in general),
  • and they can be your claim to fame and fortune, there are too many success stories to mention (e.g., Viaweb (Paul Graham and Robert Morris), Orbitz, emacs, and AutoCAD are just some)


    [EOPL] D.P. Friedman, M. Wand, and C.T. Haynes. Essentials of Programming Languages. MIT Press, Second edition, 2001.
    [PLPP] K.C. Louden. Programming Languages: Principles and Practice. Brooks/Cole, Pacific Grove, CA, Second edition, 2002.
    [TSS] D.P. Friedman and M. Felleisen. The Seasoned Schemer. MIT Press, Cambridge, MA, 1996.
    [MS] J.S. Miller. Multischeme: A Parallel Processing System Based on MIT Scheme. Ph.D. dissertation, Massachusetts Institute of Technology, 1987.

Return Home