[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: tail recursion proposal



Some more thoughts on tail-call optimization:

Scheme requires tail-call optimization because it doesn't have gotos.
Without gotos it is impossible to iterate along a list etc. without
consuming stack space on each iteration. The only other option would be to
use call/cc but the result would be too heavyweight and somewhat opaque.
The following (unchecked) code illustrates this:

Common lisp (expands to a (go label)):

(defun accumulate (lst)
  (for i in lst sum i))

or

(defun accumulate (lst)
  (prog ((l lst)
         (sum 0))
  loop
    (if (null l)
        (return sum))
    (setq sum (+ (car l) sum))
    (setq l (cdr l))
    (go loop)))

Scheme (tail recursive):

(define (accumulate lst)
  (let loop ((l lst)
             (sum 0))
    (if (null? l)
        sum
        (loop (cdr l) (+ (car l) sum)))))

Scheme (using call/cc for goto)

(define (accumulate lst)
  (let ((l lst)
        (sum 0))
    (let ((k (call/cc (lambda (k1) k1))))
      (if (null? l)
          sum
          (begin (set! sum (+ (car l) sum))
                 (set! l (cdr l))
                 (k k))))))

On the other hand contrast the following code:

Lisp:

(for i to 10 collect #'(lambda () i))

or

(do ((i 0 (+ i 1))
     (lst nil (cons #'(lambda () i) lst)))
    ((= i 10) (nreverse lst)))

with scheme:

(let loop ((i 0)
           (lst '()))
  (if (= i 10)
      (reverse lst)
      (loop (+ i 1) (cons (lambda () i) lst))))

or

(do ((i 0 (+ i 1))
     (lst '() (cons (lambda () i) lst)))
    ((= i 10) (reverse lst)))

In the lisp case a loop variable is rebound causing the returned list of
functions all to return the same value. The scheme examples generate new
names at each iteration and these examples return a list of functions each
returning a different value.

The point to all this is that lisp almost always requires one to
side-effect variables to write constant-space loops while scheme allows one
to write constant-space loops without side-effects if there is tail-call
optimization. "set!" is not as essential when writing scheme programs.

I don't know whether or not any of these points need to be brought up in
the discussion of tail call optimization.

--Sidney Marshall


At 01:01 PM 1/8/98 PST, Kent Pitman wrote:
I frequently run across Scheme refugees who are confused by an
expectation that all programming languages will support tail recursion
as Scheme does.  Indeed, it's a common source of programming error for
Common Lisp users who used to be Scheme people and who are often
unable to express themselves iteratively using a "loop" notation, and
so end up blowing out of stack space.  (Always ironic, to me, because
I thought the original point of the tail recursion exercise was to
give programmers flexibility about expression, not to trade one form
of dogma for another...  I would feel more sympathetic toward them if
they knew what a loop was and were just grumbling about the expressive
inconvenience, but they often look at me with a stare like there is
something dirty or unhealthy or even just plain unknown about "loop"
constructs.)  Anyway, I trace the cause of this inflexibility on their
part partly to the fact that I think S&ICP and its followers apparently
spend too little time on the "alternatives" part, but I trace THAT, in
turn, to this part of the Scheme spec which simply and without explanation
requires a certain behavior.

Now that we're going to spend time explaining things a little, I take
some specific issue with the following paragraph, which I consider
central to the issue.  It appears to express a universal truth instead
of a design trade-off.  And that, I think, is a shame.

|  Proper tail-recursion is important because it allows iterative
|  computations to be expressed efficiently, reliably, and portably
|  using ordinary procedure calls.

I think the design trade-off is certainly one made by Scheme but it is
not the only legitimate such choice, and I would prefer to see wording 
that did not fill the reader's head with Scheme-centric propaganda.

I also think the funny little list of adverbs here is odd in that they
really wants to be combining, not modifying expressed in a free-standing
way.  There is nothing inherently "unreliable" or "unportable" about the
expression of a procedure in this way.  What is unreliable or unportable
is only the space-efficiency.  The minimal fix to this paragraph's complaint
is:


|  Proper tail-recursion is important in Scheme because it allows
|  iterative computations to be expressed using ordinary procedure
|  calls with a space efficiency that is portably reliable.  

But I would prefer more elaborated text, such as:

|  The requirement to support proper tail recursion implicitly thwarts
|  some techniques for ``stack debugging'', since if an error occurs,
|  the chain of active continuations will have lost information about
|  how the computation reached the present state.  This was a
|  consciously made design trade-off.  Proper tail-recursion has
|  traditionally been important in Scheme because it allows iterative
|  computations to be expressed using ordinary procedure calls with a
|  space efficiency that is portably reliable.  Also, sufficient
|  techniques are believed to exist so that it is usually possible for
|  an implementation to compensate heuristically for the would-be loss
|  of debugging power.