In the code below, you are using the result of (decode-1 ...)
as an argument to cons
:
(define (decode bits tree)
(define (decode-1 bits current-branch)
(if (null? bits)
'()
(let ((next-branch
(choose-branch (car bits) current-branch)))
(if (leaf? next-branch)
(cons (symbol-leaf next-branch)
(decode-1 (cdr bits) tree)) ;; << here
(decode-1 (cdr bits) next-branch)))))
(decode-1 bits tree))
The result of the current level of recursion is not directly computed by a function call (here a recursive call), but combines this result with another value to build a result.
This means that the runtime has to keep track of the local state of computation (called a frame) while computing the recursive call to decode-1
, in order to be able to build the resulting list with cons
. This makes all the recursive calls wait one for another, acting like a stack.
With a recursive terminal function, you can discard all the state of your current call before calling yourself recursively: there should be no necessary state to keep for when the recursive call return: this is the condition that allows an interpreter (or compiler) to replace the call by a jump (this need not be a recursive call, tail call elimination may happen for any function).
You can add an additional parameter to decode-1
(often called "an accumulator"), here the list you want to return. For example, you can call it res
for result. Initially it is the empty list.
When you reach (null? bits)
, you return the list computed so far, res
. The list is probably going to be in reverse order, so you may want to reverse it (this can be made recursive terminal too).
In the general case of recursion, you call:
(decode-1 (cdr bits)
tree
(cons (symbol-leaf next-branch) res))
In other words, you first build a list, compute (cdr bits)
, etc (in no particular order theoretically), then you pass all the three values to decode-1
: when this call returns, it will exactly be the return value you want to topmost call to return, so there is no need to track any other variable in the function.