Subject: Re: Was not making tail recursion elmination a mistake?
From: rpw3@rpw3.org (Rob Warnock)
Date: Sun, 13 Jun 2004 19:46:18 -0500
Newsgroups: comp.lang.lisp
Message-ID: <y5ednVdumcDHaFHdRVn-iQ@speakeasy.net>
Marco Baringer  <mb@bese.it> wrote:
+---------------
| rpw3@rpw3.org (Rob Warnock) writes:
| > But note that even in trampolined CPS form it is not necessary to use
| > THROW (which, when deeply nested, might be quite slow) to achieve the
| > desired result. Consider this slight rewrite [which, with no THROW,
| > allows replacing #'TOPLEVEL-K with #'IDENTITY]:
| 
| 1) this means that the trampolined function can never return a function. 
+---------------

Well, you *can*, but it takes a small hack to the loop and the
reintroduction of a top-level value function to use instead of
#'IDENTITY, and it also helps to change the API for RUN-TRAMP
slightly, something like this, perhaps:

    ;;; RUN-TRAMP takes a function of one argument which calls the
    ;;; desired target function with the given argument as a continuation,
    ;;; e.g., (lambda (k) (foo arg1 arg2 ... k))
    (defun run-tramp (func)
      (let ((cookie nil))
	(flet ((top-k (v) (setf cookie v) nil))
	  (loop for f = (funcall func #'top-k) then (funcall f)   
		while (functionp f)
	    finally (return (or cookie f))))))

[Note: The (RETURN (OR COOKIE F)) is unnecessary; it could just be
(RETURN COOKIE). I used the overly-defensive OR in case some called
function directly returned a non-function value instead of a trampoline
continuation.]

The function LEN-TRAMP remains the same as in my last reply, but
instead of starting it like this:

    (run-tramp (lambda () (len-tramp '(a b c d e) #'identity)))

now you call it like this:

    > (run-tramp (lambda (k) (len-tramp '(a b c d e) k)))
      0: (LEN-TRAMP (A B C D E) #<Interpreted Function TOP-K {48536249}>)
      0: LEN-TRAMP returned #<Interpreted Function "LAMBDA (LIST K)" {4853AF89}>
      0: (LEN-TRAMP (B C D E) #<Interpreted Function "LAMBDA (LIST K)" {4853B571}>)
      0: LEN-TRAMP returned #<Interpreted Function "LAMBDA (LIST K)" {4853BEE9}>
      0: (LEN-TRAMP (C D E) #<Interpreted Function "LAMBDA (LIST K)" {4853C4B1}>)
      0: LEN-TRAMP returned #<Interpreted Function "LAMBDA (LIST K)" {4853CE09}>
      0: (LEN-TRAMP (D E) #<Interpreted Function "LAMBDA (LIST K)" {4853D3F1}>)
      0: LEN-TRAMP returned #<Interpreted Function "LAMBDA (LIST K)" {4853DD29}>
      0: (LEN-TRAMP (E) #<Interpreted Function "LAMBDA (LIST K)" {4853E2F1}>)
      0: LEN-TRAMP returned #<Interpreted Function "LAMBDA (LIST K)" {4853EC09}>
      0: (LEN-TRAMP NIL #<Interpreted Function "LAMBDA (LIST K)" {4853F1D1}>)
      0: LEN-TRAMP returned #<Interpreted Function "LAMBDA (LIST K)" {4853FAA1}>
    5
    > 

Now if you want to return a function, you can do like this [with apologies
for the continual wrapping & unwrapping of CONSTANTLY forms, just to get
a function result, but it does let the code resemble LEN-TRAMP's]:

    > (defun func-val-tramp (count k)
	(if (zerop count)
	  (lambda () (funcall k (constantly 0)))
	  (lambda ()
	    (func-val-tramp (1- count)
			    (lambda (v)
			      (funcall k (constantly (1+ (funcall v)))))))))
    > (trace func-val-tramp)
    > (run-tramp (lambda (k) (func-val-tramp 3 k)))
      0: (FUNC-VAL-TRAMP 3 #<Interpreted Function TOP-K {485E5B41}>)
      0: FUNC-VAL-TRAMP returned
	   #<Interpreted Function "LAMBDA (COUNT K)" {485EA751}>
      0: (FUNC-VAL-TRAMP 2 #<Interpreted Function "LAMBDA (COUNT K)" {485EAD51}>)
      0: FUNC-VAL-TRAMP returned
	   #<Interpreted Function "LAMBDA (COUNT K)" {485EB631}>
      0: (FUNC-VAL-TRAMP 1 #<Interpreted Function "LAMBDA (COUNT K)" {485EBC31}>)
      0: FUNC-VAL-TRAMP returned
	   #<Interpreted Function "LAMBDA (COUNT K)" {485EC501}>
      0: (FUNC-VAL-TRAMP 0 #<Interpreted Function "LAMBDA (COUNT K)" {485ECB01}>)
      0: FUNC-VAL-TRAMP returned
	   #<Interpreted Function "LAMBDA (COUNT K)" {485ED3F1}>
    #<Closure Over Function "DEFUN CONSTANTLY" {485EDB49}>
    >

So we got a function as a result, o.k.? Now let's call it and see what
the real result is:

    > (funcall *)

    3
    >

+---------------
| 2) the throw will never have to search more than two stack frames
|    (shortening the stack is the whole point of trampolining).
| 
| afaict. am i missing something?
+---------------

Not really. It's just a difference of perception in the cost of THROW
(which I assume to be high, even across a small number of stack frames)
versus an extra FUNCALL or two.


-Rob

-----
Rob Warnock			<rpw3@rpw3.org>
627 26th Avenue			<URL:http://rpw3.org/>
San Mateo, CA 94403		(650)572-2607