0:00

Last week, you have gotten to know functions as essential building blocks of

programs. This week, we'll still be concerned with

functions. We will get to know several ways to

construct them and to compose them. Finally, next week, we are going to look

at data and objects. In the first session of this week, we are

going to look again at recursion, and we will find out that there are actually

several ways to express recursion. First let's review function application.

You know from the last session that there's one simple rule.

You evaluate a function application F applied to arguments E1 to EN

by evaluating the arguments, expressions E1 to EN they would resulting the values

V1 to VM say. And then replacing the application of the

function by its body. And at the same time replacing the forming

parameters of the function by the actual arguments V1 to VM.

1:01

This rewriting rule can be formalized by rewriting the program itself.

Say, you have a program with a function definition def f with parameters x one to

x n and a body b. And then, somewhere else eh, you have a

function call f applied to value argument values v 1 to v n.

That program can be rewritten to a programme that contains the same function

definition but the function application has now being replaced by the body of f,

B, and at the same time the former parameters x 1 to x n have been replaced

by the argument values. And the rest of the programme is assumed

to be unchanged, which is assumed to be these triple dots here.

That's areas of the programs that we do not touch.

This notation, we want for x 1 the end for x n is called a substitution.

What we mean by it, is that all occurrences in b of the values x one to x

n have been replaced by the corresponding values v1 to vn.

Here's an example, consider GCD, the function that computes

the greatest common divisor of two numbers.

We know an implementation for GCD it is since a way long time, since ancient

greek, its known as Euclid's algorithm. To compute the greatest common divisor of

two numbers A and B, what do you do. First if B is zero then A is a common

divisor of both A and B and otherwise you compute the greatest common divisor of.

First b, and then a modular b. Percent sign here is as in Java and C,

means that A modular B, the modulus of A and B.

3:20

Next you simplify the if-then-else using the rule we have developed previously.

So if false, fourteen else, GCD of 21 and fourteen modular,

21 would give the else part. Once we have the else part, we reduce its

arguments to values. That gives us an other call to GCD which

we expand, and the process repeats. I have signified that here with a multiple

errors to say these are reductions that take more than one step.

We get GCD fourteen to seven, we get GCD seven, zero, and finally you get the

condition if zero equals zero, seven else some other call to GCD.

And that finally, since the condition is true here, we get seven.

4:13

Okay. Let's look at another rewriting example.

Factorial. Classical algorythm for factorial is the

following. You're said to take the factorial of a

number n. You ask again if n equals zero,

then the factorial would be one. Otherwise it would be n times factorial of

n minus one. Let's apply that algorythm to factorial of

four. So that would expand to the following

conditional. If four equal zero, one, that's four times

factorial four minus one. You evaluate the if then else, that gives

you in a couple of steps, four times factorial of three.

The second part. Reducing further gives you four times

three times factorial of two. And so on, until you finally reach four

times three times two times one times one. That was the last, the first case of

factorial and that reduces to 120. Question,

what is the difference between these two sequences?

5:18

GCD and factorial? Well, one important difference is that if

we come back to GCD, we see that. The,

reduction sequence essentially oscillates. It goes from one call to GCD to the next

one here, to the next one here, to the next one here and finally it terminates.

In between we have expressions that are different from the simple call like if

then else's but we always come back to this shape to the call of

GCD. If we look at factorial, on the other hand

we see that in each couple of steps we add one more element to our expressions.

Our expressions becomes bigger and bigger until we

end when we finally reduce it to the final value.

6:14

So, that difference in the rewriting rules actually translates directly to a

difference in the execution, in the actual, execution on a computer.

In fact, it turns out that if you have a recursive function that calls itself as

its last action, then you can reuse the stack frame of that function.

This is called tail recursion. And by applying that trick, it means that

a tail recursive function can execute in constant stuck space, so it's really just

another formulation of an iterative process.

Could say a tail recursive function is the functional form of a loop, and it executes

just as efficiently as a loop. So if we go back to GCD, we see that in

the else part, GCD calls itself as its last action.

And that translate its, to a rewriting sequence that was essentially constant

size, and that will, in the actual execution on a computer, translate into a

tail recursive call that can execute in constant space.

On the other hand, if you look at factorial again, then you'll see that

after the call to factorial n minus one, there was still work to be done, namely,

we had to multiply the result of that call with the number N.

So that call here is not a tail recursive call, and it becomes evident in the

reduction sequence, where you see that actually there's a buildup of intermediate

results that we all have to keep until we can compute the final value.

So that factorial would not be a tail recursive function.

Both factorial and GCD only call itself

but in general, of course, a function could call other functions.

So the generalization of tail recursion is that, if the last action of a

function consists of calling another function, maybe the same, maybe some other

function. The stack frame could be reused for both

functions. Such calls are called tail calls.

After having gone through the exercise, you might ask yourself, should every

function be tail recursive? Well, not really.

The interest of tail recursion is mostly to avoid very deep recursive chains.

Most implementations of, the JBM, for instance, limit the maximal depth of

recursion to a couple of thousand stack frames.

So if your, the input data is such that these deep recursive chains could happen,

then yes it's a good idea to reformulate your function to be tail recursive, to run

in constant stack frame, so as to avoid stack overflow exceptions.

On the other hand, if your input data are not,

susceptible to deep precausive chains then

clarity trumps efficiency evert time, so write your function the clearest way you

can. Which often is not terricosive.

And don't worry about the steck frames that are spent.

As Donald Knuth has said, premature optimization is the source of all evil.

And that's the model that's very valuable to follow.

9:26

Coming back to factorial. You might have observed that factorial

grows really very, very quickly. So, even after very low number of

recursive steps it will already the, exceed the range of integers, or even long

integers. So, in that case definitely it was not

worth making factorial a tail recursive function.

So, we did that only as a, an exercise so that you have the techniques ready when

you need them. So let's do an exercise on tail recursion.

You've seen factorial, and we've seen that, that version of the function was not

tail recursive. So now the task is to design a tail

recursive version of the same function. Okay,

so the most convenient way to solve this exercise is using another worksheet.

I have already created a new package in our proc fun project called week two, and

in that package we have a worksheet excercise in which I have

already written the signature of factorial but I've left out the implementation.

So, how would we implement that in a tail recursive fashion?

Actually, it turns out that we need another function, called loop, that takes

two parameters, not one. I'll give you an outline of loop here.

So, the idea is, it would take an accumulator.

That's also an int, and the current value and it would ask if, the current value is

zero then it wouldn't return one as before.

But the accumulator and otherwise, it would return something

else. And we would start the program with loop

of an initial value of the accumulator and the current value of n.

So the question is, what do we need to do to fill in the question marks here?

11:21

The first task is to fill in something for the initial value of the accumulator.

Here it's pretty clear that, we say, well, if n equals zero then the accumulator is

returned so it has to be the right value for n equals zero.

And that would be one. So we pass one here.

The second question is, what do we do in the case where n is not equal to zero.

And the idea there is we would call again into the loop with the accumulator times

and the current value and n reduced by one.

And the thing still doesn't compile because we have forgotten to give a result

type to loop. Once we do that, factorial compiles.

And we can test it so let's do that.