0:00

, I wanted to show you one more idiom that helps us avoid repeated computations and

how it's useful. Turns out this idiom does not really use thunks, but that's okay

it's still worth knowing and it's called memoization which is not an English word.

But it is a technical word we use for this. And we really do leave the r out and

call it Memoization not memorization and it's been called that for a long time. So

here's the idea, if a function has no side effects and doesn't read anything that can

change. There's no point in computing it twice for the same argument. If you call

it twice with seven you're going to get the same answer. If you call it twice with

the same lists you're going to get the same answer. Doesn't matter how many times

you call it. So what we could do is keep what's called a cache of previous results.

Keep some data structure that remembers. Hey if you call this funciton with seven,

this is what you're going to get back. And this can be a great idea if maintaining

and using the cash is cheaper. Then recomputing the results. And if people

actually do the recomputations. They actualy call the same function with the

same argument again. Otherwise, the cache is a waste of space and a waste of time.

So, this idea is really pretty similiar to promises. That thing we saw with force and

delay, right? That when we do the first force, we'll store the result so any later

force just has the result there. But force of delay was for thunks that don't take

any arguments. Once you take arguments, you can't just have one thing that you've

stored for future reference. You need a table of them, depending on what the

arguments are. So that's where memoization is a little more sophisticated and I'm

going to show you an example where using memoization with a recursive function

actually leads to a program that is exponentially faster. And so it's a common

technique, something you can apply almost mechanically. That could actually lead to

programs that are much more efficient. other times, the y're just a little more

efficient, and it's still considered a convenient thing to do. So I'm going to

show the most of the rest of this with the code. I'm going to use this classic

example from memoizaton, this is what most people use. Here's an implmentation the

fibonacci function. This is a function for mathematics the same way we've been using

factorial, which says that the factorial n is n times factorial of n minus 1. With a

base case of factorial 0 as 1. Fibonacci is a little more sophisticated. It says

fibonacci is one for both 1 and 2. So just read the racket code here. If x is one or

x is 2, it's 1. Otherwise it's the sum of fibonacci of x minus 1. And Fibonacci of x

minus 2. So you see two recursive calls here. And turns out these two recursive

calls cause this to be a very inefficient implementation. This is exactly how it's

defined mathematically. You recall Fibonacci 1 with 30 you get a nice big

number like 830,000. But if you call with 40, this takes about a thousand times

longer to evaluate. And I don't have a thousand times longer to wait so I'm just

going to stop this. And say this is not a good implementation of the fibonacci

function. Okay so, what are we going to do about it? One thing you could do, which

I'm not going to focus on here, is throw away that algorithm, and implement this

other bottom up algorithm, that uses a local helper function that takes an

accumulator and starts at low numbers. And figures out fibonacci up to high numbers

by adding the 2 previous numbers as you go along. You're welcome to look at the code,

it's not that complicated. I just want to admit that this algorithm exists before

moving on and showing you a different way to do this efficiently that show this idea

of memoization. 'Kay, so, we're going to forget about that second version. Just

move on to the third version. I know it looks big and complicated. We're just

going to go through it line by line and see that it's just implementing this idea

of keeping around all the previous results. So, any time we compute Fibonacci

of anything we're going to put that in our table. And because we're going to do that

even when we make recursive calls it's going to in turn Fibonacci into an

effecient. Function. So let's see how that works. So the first

thing that you'll notice is that fibonacci ends up just being this helper function f.

So I'm going to define this function f here and then just return it. And the

reason why I'm doing that, is that I need f to use this memo table, this cash, this

thing that is initially null but is bound to this variable memo. It's essential, I

don't want memo to be at the top level, I don't want to define memo up here, 'cause

this is an implementation detail. I don't want to expose it to the outside world. ,

4:48

But I also cannot put it inside this function I'm going to define. 'Cause if

you put it inside, we're going to recreate a new table every time we call this

function. Because we do not execute function bodies until we call them. We

need it outside the table, so that it persists, so it's still around from one

call to the next. And it's initially null. Now, we're going to use mutation to update

it appropriately. Really. Okay, so when we want ca, find fibonacci

of x, so we just call x right here, okay? The first thing we're going to do is see,

is it already in the memo table. So, here, I'm using a little library function,

assoc, that takes in something and a list and tells you if that something is in the

list in the following way. Let me show you how this works. You can look it up on your

own in the manual, if you don't get this quickly. So let me define a little list

here. it has to be a list of pairs. Because pairs could have anything you want

in them, but how about just, three different pairs of integers here. Okay.

So that's a little list, xs just looks like this. What assoc does is it takes a

number like 3. And a list of pairs and it checks the car fields of the pairs for

something equal and with the first time it finds one, it returns that pai r. So a

assoc 3 xs returns 3, 4. Assoc 1 xs returns 1, 2 and if it's not in the list,

like 6 you do see a 6 up here but it only looks at the car fields, it returns false.

Meaning I did not find it in the list. So that's assoc. We're going to use assoc up

here, so I had to tell you how it works. So our memo table, which is initially

null, is going to be a list of pairs, and what it's going to be is argument dot

result. We're going to store in that list pairs of, if fibonacci, fibonacci of arg

is result. Alright? So here's what we do. If we find our answer in the table, so we

gotta pair here, and everything that is not false is true, so this works, and

return the cdr of that pair. We looked up the argument x, we found it, return the

result. So that's how we get our reels. In this else branch, we didn't find it. So

now all we have to do is compute it and then put it in the table so people can

find it in the future. So, how do we compute it? That's what this let variable

does, new-ans. We say, well, if x is 1 or 2, then it's 1. Otherwise it's adding

recursive call of x minus 1 and recursive call of x minus 2. Before we return,

change via set! memo to hold a bigger list. Take the old list memo, cons on to

it the pair of our new argument result pair. So that in the future, if anyone

needs fibonacci of x They'll find it in the table. After we've done that, return

this thing that we computed and that is the fibonacci of the number. So, this all

works and what might surprise you is that it's fast. I can ask fibonacci 3 of 1000

and I get this big number. No time at all. So why is this so much efficient than the

naive recursive version, that couldn't even do 40. I don't even think it could do

35. It looks like if it's not in the table, we're still doing these two

recursive calls, and it would blow up exponentially. But we are doing two

recursive calls, whichever one we do second. Will be very, very fast. See, the

problem with exponential blowup is that we make two recursive calls, and they m ake

two recursive calls, and they make two recursive calls, and it blow ups, blows up

2, 4, 8, 16 and 32. But what happens here is we make two recursive calls. The first

recursive call ends up filling up the table with lots of numbers. The second

recursive call ends up finding what it needs in the table. Especially if the

second one is with x minus 2. The second one is x minus 1. It will still have to do

one more recursive call, but then it will find what it needs in the table. And

that's only at the top level. After that, everything's already in the table. So

recursivly, at every step, instead of making two expensive recursive calls, we

make one recursive call. And then, for the next recursive call, everything is already

in the table. So this isn't just twice as fast, it's twice as fast at every level.

And in fact, it's taking something that would take time proportional to 2 to the

x. And turning it to something that takes time proportional to x. Or if you really

want to be picky, since I have to run down this list doing assoc, maybe x squared.

But x squared for a 1000 is nothing. that's no problem at all. Whereas, 2 to

the x, where x is a 1000, is more than I believe, the number of particles in the

universe. So that's our example. It's just a programming idiom. All we did was really

that something that had nothing with fibonacci itself. We created this table.

We always checked it first. If we found it, we did nothing else. Otherwise, we

compute our answer. And before we return, we add our result to the table for the

future. And that is Memoization.