Tail recursion is the act of making a tail recursive call.
We can understand that term in parts.
A call is just application of a function.
A recursive call occurs when a function invokes itself.
A tail call occurs when a function's result is just the value of
another function call. In other words, in order to determine the value of
function f, we call g, and return whatever g
did, without modification.

The definition of foo contains a recursive call to foo
(it's recursive because foo is the procedure in which the call to
foo appears) and a tail call to bar. The call to
bar is a tail call because, if the parameter x is even,
then the value of the call of foo is just whatever bar
returns.

If you step through (foo 8) in Donkey, you will see that the
additions of 1 pile up: we keep wanting to evaluate (+ 1
<something>),
but first we must evaluate the
<something>. On the other
hand, stepping through (foo 7) shows that the value of the call to
foo is just whatever the value of (bar 7) is.

For a non-tail call, Scheme must remember that after the current function
application, it is responsible for doing something else (in this case,
adding 1 to the result); and there may be many such things to do. For a
tail call, Scheme doesn't have to remember anything, because there is
nothing more to do: after it is done with the current function call, it
is really done.

Non-tail calls force Scheme to remember future actions (that couldn't be
performed before), but tail calls don't. Non-tail calls require more space
(because there is more to remember), so they are not as efficient as tail
calls. Tail calls also permit us to forget the values of function
parameters and local variables, because we never do any more work in the
calling function, but a non-tail call forces us to remember such values,
which might be used when we return to the calling function.

We've noted that tail calls save space, but they also save time (are faster
than non-tail calls) for much the same reasons.

Step through the evaluation of (Even? 5). (You may want to set
Donkey's Stop-at menu to apply to make this task less
tedious.) Notice that the calls to not keep piling up (in the
first case), or the unfinished
cond expressions keep piling up (in the second case). The
recursive call to Even? is not tail-recursive.

Change the definition of Even? to use another function
Odd? which is defined in much the same way.

Now step through evaluation of Even? again. Notice that the
expressions to be evaluated don't grow: (Even? 5) becomes
(Odd? 4), which becomes (Even? 3), etc. Nothing remains
to be done to the results of those calls.

Use Donkey to step through the evaluation of both of these functions on
(list 1 2 3 4).
Notice the order in which the additions are done: the original version did

(+ 1 (+ 4 (+ 9 (+ 16 0)))) => 30

while the accumulator-style version did

(+ 1 0) => 1(+ 4 1) => 5(+ 9 5) => 14(+ 16 14) => 30

The additions were done in the opposite order by the two versions. In the
case of addition, that is perfectly harmless, but for operations like
cons, you need to be a bit careful to get the resulting list in
the right order.

You can automatically convert an ordinary recursive function into a
tail-recursive one by following these steps:

Turn the original function into a local helper function.

Add an accumulator argument to the helper function.

Change the helper function's recursive call into a tail-recursive
call; be sure to update the accumulator appropriately.

Make the body of the main function just a call to the helper, with
appropriate initial values.

Not every recursive function can be turned into a tail-recursive function.
In particular, if a function makes a recursive call, but then examines the
result and does different things depending on its value, then it may not be
possible to make the function tail-recursive. But if we always do the same
thing to the recursive call result, no matter what it is (as in
sum-of-squares, when it is always added to (* (car
num-list) (car num-list))), then it is generally possible to make the
function tail-recursive.

I claim that whenever sos-helper is called, its first argument
remaining is a list which is a suffix of the parameter
num-list of sum-of-squares-tail-recursive, and that its
second argument sum-so-far is the sum of the squares of the
elements a prefix of num-list; and furthermore, the noted suffix
and prefix together make up the entire list.

In order to prove this claim, let us examine each call to sos-helper.
First, consider the call in the body of the main function. The first
argument is num-list itself; that is a suffix of
num-list, albeit an improper one. The second argument is the sum
of the squares of a suffix of num-list: the (degenerate) empty
suffix. Together this prefix and suffix comprise all of num-list.

Now only the recursive call needs to be considered. Suppose that when we
get to the recursive call, the current call satisfies the property, so
remaining and sum-so-far account for the two parts of the
list. (Induction, which you should have seen in your math classes so far
and of which you will see more in Comp 280, permits this assumption.) The
first argument for the recursive call is (cdr remaining); this is
a suffix of remaining, and since remaining is itself a
suffix of num-list, then (cdr remaining) is also a
(shorter) suffix of num-list. Now consider the other argument to
sos-helper: (+ (* ... ...) sum-so-far).
sum-so-far is the sum of the squares of a prefix of
num-list (i.e., all the elements not in remaining,
and this call to + produces the sum of the squares of a longer
prefix: the elements not in remaining, plus (car
remaining). So the arguments to the recursive call satisfy the property
too.

A property which is true of variables and data at a certain point in your
code is often called an invariant. Reasoning about invariants can be
invaluable in understanding your programs and proving them
correct--either informally, to yourself, or mathematically. In fact,
I probably would have inserted a comment in the definition of
sum-of-squares-tail-recursive noting the invariant on the use of
the helper function, to aid my understanding the next time I examined the
code.

It is a helpful exercise to understand why this works; if you don't
understand, ask a labby or use Donkey to step through its execution.

while-fn takes two arguments, each of which is a procedure of no
arguments. (A procedure of no arguments is commonly called a thunk.)
You can think of while-fn as evaluating its body over and over.
This repetition (looping) doesn't stop until the test is false.
In other words, as long as the test evaluates to true, the body is evaluated.
So the execution order is test, body, test, body, test, ..., test (until
finally test evaluates to false, which is the last evaluation of test, and
body is not evaluated again). The value returned by evaluation of the body
is always ignored; the body is evaluated only for side effect.

To transform a tail-recursive function into a while loop, follow the
following steps. (There is not an automatic function for converting an
arbitrary recursive function into a while loop--you have to convert it
into a tail-recursive function first.)

Turn the helper function parameters into local variables; their
initial values are the same as the initial values for the call to the
helper function from the main function.

The continue test is the same as the test in the helper function, for
the direction that causes a tail-recursive call to be made. (If the
tail-recursive call appears in the else part of the if, then add
a call to not.)

The body uses set! to update the value of each variable; the
values are the same as the arguments passed to the tail-recursive call.

While looks a little strange, because its arguments are wrapped in
(lambda () ...) rather than having the function bodies ``naked''.
The reason for this is that we don't want to evaluate the function bodies
once and for all time--rather, we want to check the test expression
anew each time that the body is evaluated, and we want to run the body
multiple times. We can do that via a function call.

Suppose we wanted to define a function if-fn which acts just like
if; for instance,

The problem is that functions (such as if-fn) always evaluate all
their arguments, while special forms (such as if and
cond) may only evaluate some of their arguments, leaving others as
expressions rather than values. We can't use the ordinary part of Scheme
to write new special forms, only new functions.

That is one reason we wrapped up the arguments to while-fn in
thunks: so that we didn't evaluate them as soon as while-fn
was called. The other reason was so that we could evaluate them
repeatedly, which isn't of interest for if-fn.