When you need a tail call optimization like structure in a language that doesn't (necessarily) provide it, but does provide closures, you can use a trampoline to achieve constant stack space (with a trade off for heap space for closure objects, of course). This isn't quite the same as what you asking for, but you might find it useful. It's pretty easy to implement in Common Lisp:
(defstruct thunk closure)
(defmacro thunk (&body body)
`(make-thunk :closure (lambda () ,@body)))
(defun trampoline (thunk)
(do ((thunk thunk (funcall (thunk-closure thunk))))
((not (thunk-p thunk)) thunk)))
To use the trampoline, you just call it with a thunk that performs the first part of your computation. That closure can either return another thunk, or a result. If it returns a thunk, then since it returned the initial stack frame is reclaimed, and then the closure of returned thunk is invoked. For instance, here's what an implementation of non-variadic mapcar might look like:
(defun t-mapcar1 (function list)
(labels ((m (list acc)
(if (endp list)
(nreverse acc)
(thunk
(m (rest list)
(list* (funcall function (first list)) acc))))))
(m list '())))
When the list is empty, we get an empty list immediately:
CL-USER> (t-mapcar1 '1+ '())
NIL
When it's not, we get back a thunk:
CL-USER> (t-mapcar1 '1+ '(1 2))
#S(THUNK :CLOSURE #<CLOSURE (LAMBDA #) {10033C7B39}>)
This means that we should wrap a call with trampoline (and this works fine for the base case too, since trampoline passes non-thunk values through):
CL-USER> (trampoline (t-mapcar1 '1+ '()))
NIL
CL-USER> (trampoline (t-mapcar1 '1+ '(1 2)))
(2 3)
CL-USER> (trampoline (t-mapcar1 '1+ '(1 2 3 4)))
(2 3 4 5)
Your example code isn't quite enough to be an illustrative example, but
(defun fn (x)
(when x
(princ x)
(jump 'fn (cdr x)))
(rest))
would become the following. The return
provides the early termination from fn
, and the thunk value that's returned provides the “next” computation that the trampoline would invoke for you.
(defun fn (x)
(when x
(princ x)
(return (thunk (fn (cdr x)))))
(rest))
(return-from fn (fn (cdr x)))
first calculates(fn (cdr x))
(and so causes recursion) and then returns its value as the result; what you want is to return first (control-wise), and then calculate the returned value (avoiding the recursive build-up of the stack). This answer shows how. – Will Ness