trampoline

picoLisp does not support tail call optimisation. However, there is a way around it, using a trampoline:

   Used in some LISP implementations, a trampoline is a loop that
   iteratively invokes thunk-returning functions. A single trampoline
   is sufficient to express all control transfers of a program; a
   program so expressed is trampolined or in "trampolined style";
   converting a program to trampolined style is
   trampolining. Trampolined functions can be used to implement tail
   recursive function calls in stack-oriented languages.

First, lets see the final code and then discuss how to use and derive it:

(def 'continue list)

(de done (R)
   (cons NIL R) )

(de trampoline (F A)
   (use R
      (loop
         (NIL (car (setq R (apply F A))) (cdr R))
         (setq F (car R) A (cdr R)) ) ) )

There was a discussion on the topic suggesting that it is better to rewrite your recursive code using loops but there could be cases when recursion is a better abstraction.

Lets suppose we have the following, mutually recursive functions (the example was taken from here):

(de f1 (N)
   (println N)
   (if (= N 0)
      (println 'Blastoff!)
      (f2 N)))

(de f2 (N)
   (f1 (- N 1)))

(f1 90000) # => crash

If we call 'f1' with sufficiently big number, picoLisp interpreter runs out of stack eventually. This need not to be so. We can rewrite the code as follows:

(de f1 (N)
   (println N)
   (if (= N 0)
      (throw 'done 'Blastoff!)
      (list f2 N)))

(de f2 (N)
   (list f1 (- N 1)))

(de run-trampolined (F A)
   (catch 'done
      (loop
         (let R (apply F A)
            (setq F (car R) A (cdr R)) ) ) ) )

: (run-trampolined f1 '(9000))
9000
8999
...
1
0
-> Blastoff!

We could add some syntactic sugar:

(def 'continue list)

(de done (R)
   (throw 'trampoline R))

(de trampoline (F A)
   (catch 'trampoline
      (loop
         (let R (apply F A)
            (setq F (car R) A (cdr R)) ) ) ) )

(de f1 (N)
   (println N)
   (if (= N 0)
      (done 'Blastoff!)
      (continue f2 N)))

(de f2 (N)
   (continue f1 (- N 1)))

(run-trampolined f1 '(9000))

Now lets try usual factorial example:

(de fact (N)
   (if (< N 2)
      1
      (* N (fact (- N 1)))))

(fact 5) # => 120

We can rewrite 'fact' using tail call recursion:

(de fact/ (N R)
   (default R 1)
   (if (< N 2)
      R
      (fact/ (- N 1) (* N R))))

(fact/ 5)

and now we are ready for a trampoline:

(de fact/t (N R)
   (default R 1)
   (if (< N 2)
      (done R)
      (continue fact/t (- N 1) (* N R))))

(trampoline fact/t '(5))

Actually, we do not need throw/catch:

(def 'continue list)

(de done (R)
   (cons NIL R))

(de trampoline (F A)
   (use R
      (loop
         (NIL (car (setq R (apply F A))) (cdr R))
         (setq F (car R) A (cdr R)) ) ) )

What happens if we call 'fact/t' directly without 'trampoline'?

: (fact/t 5)
-> (((N R)
     (default R 1)
     (if (< N 2)
        (done R)
        (continue fact/t (- N 1) (* N R))))
    4 5)

In other words, we managed to split the original recursive call into a sequence of partial computations. The result above shows what function is to be called next (after the first iteration) and what arguments should be used.

As the Wikipedia article suggests, a single trampoline is sufficient to express all control transfers of a program. That gives us very powerful tool which could be useful in some cases, especially when used together with the power of lisp to make higher level syntactic abstractions.

TODO continuations

TODO cps

TODO light-weight processes

TODO asynchronous server


This page is linked from: picoLisp

Revisions: 1 2 3 4 View source XHTMLV | RSSV

picoWiki pages can be edited by anyone at any time. Imagine a fearsomely comprehensive disclaimer of liability. Now fear, comprehensively