, 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. , 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.