I was wondering if there is a way to circumvent this problem and the first solution I found was to use memoize macro described in an excellent online documentation for newLISP, Code Patterns in newLISP:
(define-macro (memoize mem-func func) (set (sym mem-func mem-func) (letex (f func c mem-func) (lambda () (or (context c (string (args))) (context c (string (args)) (apply f (args))))))))
You can apply this macro to any function with any number of arguments. The trick here is that each time a function is called its result is cached in memory for another call. It can speed up your application tremendously, which can be observed by comparing the execution time of these example Fibonacci functions:
(define (fibo n) (if (< n 2) 1 (+ (fibo (- n 1)) (fibo (- n 2))))) (memoize fibo-m (lambda (n) (if (< n 2) 1 (+ (fibo-m (- n 1)) (fibo-m (- n 2)))))) (time (fibo 35)) (time (fibo-m 35))
On my laptop (fibo 35) took 12,98 seconds to execute, while (fibo-m) executed in 0,016 miliseconds.
Unfortunately the memoize macro cannot help you to handle mutual recursion. A classic example of such recursion looks as follows:
(define (f1 n) (println n) (if (= n 0) (println "Blastoff!") (f2 n))) (define (f2 n) (f1 (- n 1)))
You can easily make newLISP run out of stack by running (f1 1000), not to mention bigger numbers. What happens if we define a "memoized" version of f1 and f2? Let's see:
(memoize f1 (lambda (n) (println n) (if (= n 0) (println "Blastoff!") (f2 n)))) (memoize f2 (lambda (n) (f1 (- n 1))))
Again, running (f1 1000) immediately exhausts newLISP's stack.
A solution to this problem is using a technique called trampolining. Bill Clementson on his blog not only explained in an excellent way the concept of using trampolines, but also provided a Common Lisp implementation, which became my inspiration to write a newLISP version:
(define (trampoline fun arg) (catch (while true (let ((run (apply fun arg))) (setf fun (first run) arg (rest run)))) 'result) result)
A trampoline iteratively executes code thunks returned by a function, and this way avoids blowing out application stack. However, in order to use trampoline, the function must return continuation (a pointer to the next step) instead of value. Below is a version of beforementioned functions modified to use the trampoline:
(define (f1 n) (println n) (if (= n 0) (throw "Blastoff!") (list f2 n))) (define (f2 n) (list f1 (- n 1)))
Now you can test it with:
(trampoline f1 '(1000)) (trampoline f1 '(10000)) (trampoline f1 '(100000)) ...