0:00

In this session, we cover lazy evaluation. Roughly, laziness means do things as late

as possible, and never do them twice. We will apply laziness to streams, and

trace how it helps evaluation in a concrete stream query.

The implementation of streams that you've seen in the last session solves the

problem of avoiding unnecessary computations when the tail value of the

stream is not needed, but it suffers from another very serious potential performance

problem. And that is that if tail is called several

times the corresponding stream will be recomputed each time tail is called. And

of course that could by itself cause up to exponential blow up in program complexity.

Fortunately this problem can be avoided by storing the result of the first evaluation

of tail and reusing the stored result instead of recomputing it the second and

third times and all other times around. We can convince our self, that this

optimization is sound, since the pure function of language and expression,

produces the same result each time it is evaluated.

So instead of re-evaluating the same expression several times.

We could just squirrel away the first time we have produced the result, and reuse

that result every time. That scheme is called lazy evaluation, as

opposed to the call by name evaluation that we've seen in the last session.

And also as opposed to the strict evaluation for normal parameters and wild

definitions. Lazy evaluation is a very powerful

principle because it avoids both unnecessary and repeated computations.

In fact, it's so attractive that a programming language, Haskell, has been

built on top of it. So Haskell uses lazy evaluation by

default, everywhere. You could argue, well, why does Scala not

do it? Well, there's one, or maybe two problems

with lazy evaluation which are essentially rooted in the fact that lazy evaluation is

quite unpredictable in when computations happen.

And how much space they take. You could argue, in a abstract, pure

functional language, it shouldn't really matter when computations happen, and

that's true. But once you add mutable side effects,

which Scala also permits, even though we haven't used them in this course, you can

get into some very confusing situations. So what Scala does is it uses strict

evaluation by default, like the absolute majority of all programming languages, but

it still allows lazy evaluation of value definitions with the lazy val syntax form.

So if you wrote lazy val x equals expression, you would get a lazy

evaluation of the value x here. So what that means is that just in a call

by name evaluation that you would get with def x equals expression.

The expression here would not be evaluated immediately at the point of the finish,

and it would be delayed, will be delayed until somebody wants the first time the

value of x. But afterwards the behavior between def x

and lazy val x diverge. For def x of course you have the behavior

that every time you call x the expression is reevaluated, whereas for lazy val the

expression is reused every time except for the first one.

So let's test this understanding with an exercise.

Consider the following program. We have a function expr and it defines

three values X, Y and Z. Each definition is preceded by a print

statement that prints that the definition is now evaluated.

And then finally we have an expression that makes use of X, Y and Z, so it does Z

plus Y, plus X plus Z, plus Y plus X. If you run this program, what gets printed

as a side-effect of evaluating expr? Here you have some choices.

Is it one of these four or maybe something else?

So let's see how we would approach this problem.

When we evaluate X, probably first have to evaluate the three definitions.

We have a vowel definition here, the right-hand side gets evaluated immediately

and would print an X. The lazy vowel on the def would not be

evaluated at the point of definition, they would be delayed.

Then we would get into the result expression where we first demand the value

of Z, so that would print a Z. Then we demand the value of y so that

would force the lazy valid would print the y.

Then we would demand the value of x. This one is already evaluated, so nothing

would be printed. Then we demand the value of z again, so

that would give us another z of y again. Well, now y is evaluated.

So we would just reuse the result we've evaluated the first time around.

And finally the x is again evaluated. So the string that gets printed is x z y

z. Lazy vowels, we can adopt our

implementation of string.coms to make it more efficient.

And the change is again very simple, the only thing that changed is that instead of

a def tail equals tail, we use a lazy vowel tail equals tail.

And that's all that's needed. With the changed it means that as before,

we will evaluate tail only when it's first demanded.

But unlike it was before, we will reuse the evaluation of tail every.

Time after the first one so we will avoid the unnecessary repeated computation.

5:55

So, all this avoiding of unnecessary computations looks really great, but maybe

you're not yet convinced. How can we really be sure that our,

execution will, in fact, avoid unnecessary portions of computations?

Well, one way to be sure is to put it to a test using the substitution model.

Using evaluation with our substitution model.

Let's do that with the expression we started with.

So stream range 1,000, 10,000, filter is prime, apply one.

Let's start reducing that, and see what happens.

So the first thing that happens here is that we have to expand string range.

And here, I've given you the expanded definitions with the actual parameters

replacing the former ones. The next thing that happens is that the

if, then, else is evaluated. So that would give me the cons expression

that we see here. Let's abbreviate this expression with the

cons to C1, so what I would have is C1, filter is prime, apply, one.

7:58

And I've evaluated the tail of the C1 constant.

Now if you go back to the C1 constant what it was.

It was a [inaudible] with the stream range expression.

So when I evaluate the tail, that's what I will get.

But what I'm left with is the expression string range of 1,001, 10,000.

And then the same thing as filter is prime.

Apply one. In other words, the same expression I

started with, only instead of the 1,000, I have the 1,001 here.

And that evaluation sequence continues until I hit the first prime number, which

in this case would be 1,009. So this expression would expand by a

sequence of reduction steps, to finally stream range 1,009, 10,000, filter is

prime, apply one. I evaluate stream range again.

Is the expression. And I want to abbreviate that expression

to c2. So I'm left with c2 filters prime apply

one. I evaluate the filter function on c2, and

that gives me an, a sequence of expressions.

9:07

Cons 1,009, and then this here, because 1,009 is a prime number, so it would be

included in the result of filter. So the next thing to evaluate is the call

of the apply function on this cons expression here.

I've plugged in here the definition of apply, which I have given you below, it's

the usual definition where a body would expect.

So we are left with an expression, like this one here, which is an if then else to

ask whether one equals zero, which is false.

So that would simplify to the second part of the if then else, which you see here.

Now, what we need to do is we need to evaluate tail.

That would in turn, force the express tail part of these, this console.

So we would get C2 tail at filter is prime, apply zero.

The next thing to calculate, again, is the tail over here.

So that now would give us the next stream range.

The. Tail part of c2.

Again, filter is prime apply zero. So what we see is we again, left with

essentially, the expression we started with, only now we have.

1,010 here and we have zero here. Where we started with 1,000 on the left

and one on the right. So that process would continue until we

hit the second prime number, 1,013. And now the computation is about to wrap

up. So the stream range function would expand

as usual. We make it a shorthand.

Call it C3, for this expression. So we, we have C3 filter is prime, apply

zero. We apply the filter function that would

say, well, 1013 is a prime number so let's include it in the list.

Have the tile expression here. Apply zero of that.

We apply, evaulate the apply function and that would pull out the first element

1013. And that's the result of the computation.

Poof. That was quite tedious to follow that far,

but imagine how more tedious it would have been if we had to evaluate actually all

the prime numbers between 1,000 and 10,000.

Here you could convince yourself that indeed, we never look beyond 1,013, all

the other prime numbers remain undiscovered and unevaluated in this