Newsgroups: comp.lang.scheme
Path: cantaloupe.srv.cs.cmu.edu!das-news.harvard.edu!news2.near.net!MathWorks.Com!udel!princeton!nimaster.princeton.edu!blume
From: blume@beth.princeton.edu (Matthias Blume)
Subject: Re: Scheme _in_ Emacs?
In-Reply-To: harrison@sp10.csrd.uiuc.edu's message of 05 Sep 1994 13:54:50 GMT
Message-ID: <BLUME.94Sep5110232@beth.princeton.edu>
Followup-To: comp.lang.scheme
Originator: news@nimaster
Sender: news@Princeton.EDU (USENET News System)
Nntp-Posting-Host: beth.cs.princeton.edu
Organization: Princeton University
References: <RAMSDELL.94Aug17063434@triad.mitre.org> <TTHORN.94Aug17150829@ceres.daimi>
	<RAMSDELL.94Aug24085039@triad.mitre.org>
	<WGD.94Aug25012615@martigny.ai.mit.edu>
	<TFB.94Aug31020302@oliphant.cogsci.ed.ac.uk>
	<WGD.94Sep3205634@martigny.ai.mit.edu>
	<TFB.94Sep5132531@burns.cogsci.ed.ac.uk>
	<HARRISON.94Sep5085451@sp10.csrd.uiuc.edu>
Date: Mon, 5 Sep 1994 15:02:32 GMT
Lines: 187

In article <HARRISON.94Sep5085451@sp10.csrd.uiuc.edu> harrison@sp10.csrd.uiuc.edu (Luddy Harrison) writes:

   > (Tim Bradshaw) writes:
   >    From: tfb@cogsci.ed.ac.uk (Tim Bradshaw)

   >    * William G Dubuque wrote:
   >> Could someone clarify just why they think it is impossible to obtain
   >> proper tail-recursion in GNU Emacs lisp? Perhaps the concern is about
   >> efficiency vs. implementability.

   >>>What I meant was is that there is no way for an implementation of a
   >>>language with elisp's semantics (ie dynamic scope) to *optimize* tail
   >>>calls in the way that scheme requires.  You can't optimize a tail call
   >>>by throwing away the stack frame of the function because the function
   >>>you call can legitimately refer to the bindings you have made.  That
   >>>was what my example was doing: the tail call to scumble can't be
   >>>optimized.  That call could equally well be a recursive call to foo of
   >>>course:
   >>>
   >>>    (defun foo (rec)
   >>>      (if rec
   >>>	  bar
   >>>	(let ((bar 3))
   >>>	  (foo t))))

   A similar effect can be achieved in Scheme using closures:

   (define foo (lambda (x f)
      (if (> x 1000)
	  (f)
	  (let ((bar (+ x 1)))
	    (foo bar (lambda () (* bar 10)))))))

   so that the objection

	   You can't optimize a tail call
	   by throwing away the stack frame of the function because the function
	   you call can legitimately refer to the bindings you have made.

   would apply to Scheme as well, if variable bindings were stored on the stack
   in general.

The point is that with a lexically-scoped language like Scheme it is
statically decidable (e.g. during byte-code compilation), which
variables ought to be saved.  In your example it is clear by looking
at the code that the inner LAMBDA form needs to build a closure
containing BAR.  It is also clear, which BAR has to be saved.

In a dynamically scoped language *all* local variables might possibly
be subject to access from another function, and there is nothing a
compiler can predict about it (in general).

In the Scheme-example above the actual call to FOO is tail-recursive
(i.e. the result of the call to FOO is the final result of the current
function).  Therefore, it is not necessary to save the return address,
it is not necessary to push anything onto the stack, and the old
return address (for the return to the initial caller) can be passed on
directly.

The fact that BAR has to be saved in a closure for the inner LAMBDA
has nothing to do with that.

   In my opinion it has never been made adequately clear precisely what
   the standard mandates in the way of tail-recursion optimization.  Some
   time ago I asked for a clarification, but as I recall, no very
   satisfactory conclusion was reached.

I think, what the standard mandates is what I just described.  The
implementation must implement tail-recursion in a way that let's it
run in constant space.  Of course, this ignores the fact that the
recursion (tail or not) can grow some data structure as it passes it
on and on through the procedure arguments:

	(define (loop l)
	  (loop (append l l)))

The point is that the data-structures implicitely introduced by the
compiler or the interpreter in order to manage the control flow must
not grow infinitely, in fact, they must be bounded by a constant.

A good way to see this is by making those control-data-structures
explicit.  This is known as CPS-conversion (``continuation passing
style''):

	(define (loop x)
	  (if (done? x)
              (finish x)
	      (loop (step x))))

would (naively) translate to:

	(define (cps-loop x k)
	  (cps-done? x
	    (lambda (r1)
	      (if r1
		  (cps-finish x
		    (lambda (r2)
		      (k r2)))
		  (cps-step x
		    (lambda (r3)
		      (cps-loop r3
			(lambda (r4)
			  (k r4)))))))))

All procedures have an additional parameter ``k'', which takes a
``continuation''.  Instead of actually returning each procedure
``continues'' by passing the final result on to its continuation.

Now, how is this related to the original problem.  First, note that
all calls to all functions are now tail-recursive.  There is never any
``return'' necessary.  In fact, no function ever returns, it just
calls its continuation.  No stack is needed.

But wait -- the stack, which was implicit before, is now explicit in
our program: everything that needed to be saved on the stack is now
saved in closures for the various LAMBDA-forms, which are used to
build the continuations.  For example, at the point where we call
DONE? we still need X.  Therefore, X should better be saved.  It turns
out that X is a free variable (let's ignore ``global'' variables like
LOOP, FINISH, and STEP) in the first big LAMBDA-form:

	(lambda (r1) ...)

This means that, indeed, X will be saved in the closure for this
LAMBDA.  Futhermore, K is also a free variable of this particular
LAMBDA expression.  What is K?  K is the ``continuation'' for the
initial call to (CPS-)LOOP.  This means that K ``knows'', where to
continue after we're done -- K basically is a ``return address''!

The closure for the LAMBDA-form saves X and K -- exactly what we would
have saved on the stack in a stack-based implementation!

Now look at the rather peculiar LAMBDA-forms:

	(lambda (r2) (k r2))

and

	(lambda (r4) (k r4))

These LAMBDA expressions are so-called eta-redexes (i.e. an
``eta''-reduction can be performed).  Eta-reduction transforms
anything that looks like

	(lambda (x) (M x))

into

	M

provided M doesn't contain x as a free variable.  Therefore, both
expressions from above can be simplified to just

	k

and

	k

Now, remember that building the closure for one of those LAMBDA's
corresponds to saving the free variables onto the stack.  We would
have saved K (the return address onto the stack), but after our
simplification we don't.  Instead, we pass K (i.e. our own return
address) directly to the callee. This sounds suspiciously familiar,
doesn't it?  And in fact -- by looking at the original Scheme program
we find that both LAMBDA forms stem from a call to a function in tail
position.

The eta-reduction has the effect that instead of making up a new
return address (a new continuation), the only purpose of which is to
pass the result on to the old return address (old continuation K), we
just pass the old return address (old continuation K) to the function
to be called -- without pushing anything onto the stack.

Several implementations of Scheme, ML, and other languages employ CPS
as an intermediate language.  In his book ``Compiling with
Continuations'' Andrew Appel critizises the Scheme standard for just
mandating tail-recursion optimization, ignoring many other useful
optimizations.  Eta-reduction can be used successfully in many more
places in the CPS -- tail-recursion elimination becomes a mere
byproduct of a more general approach.

Sorry for the long post, and hopefully I haven't turned everybody off
by going into some details of CPS.

--
-Matthias
