 It surprises me every time. All right, hi, everyone. It's time for us to write a hashtag. For those of you who have not gone through this before, I'm John, in case you weren't aware. I have a Patreon page and also a Twitter page as you probably were you found this in the first place. I do a bunch of these live coding sessions of varying degrees of sort of required Rust expertise and looking at, we've looked at everything from doing easy to orchestration to asynchronous SSH to now a hash map and I'm thinking of doing some other things in the standard library. If you want to hear about upcoming streams, then you can either check Twitter, you can follow my Patreon, or wherever else you think you might find me. The plan for today is basically to implement a hash map and not quite one that's as advanced as one that's in the standard library. Our goal is not, at least for this stream, to write something that could replace what you would just use from the standard library. We're going to make a very simple sort of linked hash map. It's gonna have no fancy features, but it's going to show you just how you might build a data structure in Rust and also the kind of API you might want to expose. We'll basically try to mirror what's currently in the hash mapping collections, including stuff like the entry API if we get time for it. I'll be monitoring the chat as well. So if you have questions as we go, feel free to just do a shout out there and I will try to monitor in the background. So yeah, let's get to it. For your tweet and camera running, that's great. I appreciate it. All right, let's see. So a hash map for those of you who are not aware or a map in the first place, a map is just a key value store. So it stores mappings between keys and values. Any given key can only have a single value. And you can use the key to efficiently get access to a value that you've stored under that key previously. The way it does this is it takes a hash of the key and indexes that into an array. So basically the hash takes any object to be a string, it could be a struct of some kind, anything that implements the hash trait. And the hash trait, if we look at it, so hash is not all that useful on its own, but hash is basically a way to take a hasher, as you see here, you write a bunch of just values into it. It can be a byte stream, it can be integers or whatever. And in the end, what you get back when you finish hashing is just a single number. And so this is a way to turn any object into a single number. Of course, that number may have duplicates, so you might have two objects or two strings or two numbers that hash to the same value and we still want to be able to fetch those separately. Like if I put something under the key foo and something under the key bar, if foo and bar have the same hash, I still want to be able to get back the values from them separately. Like if I do map.get foo, I don't want to get the value for bar back in vice versa. And so the way you deal with that is you need to deal with collisions. And this is a lot of where the complexity and also where the interesting other designs you can have for hash maps come into play. So a hash map is basically just an array in a memory where every entry in that array is used to index by the hash of the key. So say that I'm trying to insert something with the key foo into the map, I would hash foo, that gives me some number. I would take it modulo the length of the array and that's the bucket as it's called, that that thing will fall in. But now you can imagine the foo and bar fall into the same bucket and now there are basically two strategies you can take. You can have what's known as a link hash map, which is what we'll be building today. So a link hash map for every bucket, you keep a list of things that hash to that bucket and then you just walk that list in some fashion to and compare the actual keys you store, compare each one until you find the key you're actually looking for. So like a hot key to a piece of info in the array. Yes, sort of. So I think those will be clear with code so we'll get to that in a second. But I just want to illustrate the difference between what we're doing and what the standard library hash map doing. So we're gonna, for every bucket where there might be collisions, where two keys map to the same bucket, we will just keep a list of all the keys that map to that bucket and we will walk that list in the standard library and in many other hash map schemes. What you do is you have what's known as open chaining where instead of adding some other, instead of every entry in that original vector being a vector itself or a list, you store any values that collide on the hash in other places and other buckets in the map. And there are many different ways to do this. So this, for example, the cuckoo hash map, which will take two hashes of every key and put it in one or the other. And then this allows you to shift elements around. We might get into that in a later live coding stream where we try to make this more fancy. But for this, we're just gonna do the very straightforward thing of keeping multiple keys for every bucket and just scan them. So what does a hash map look at? The easiest thing for us to do, and I've sort of gone through this in past live coding streams as well, is I like to start any library that I build with an example, with basically how I imagine that users of the library would write code. In this particular instance, we can use the standard library hash map because it has an example. So we can just copy and paste this example and we want that example to work with our hash map, right? So we are going to do, oh, actually, let me make this a little bit larger, maybe? Like that? That's probably better. We can adjust this if we go. All right, so we're gonna make a new lib and it's gonna be called hash map. Very original, two terminals here. And we are going to make an example. Inside of examples, we're just gonna take the standard library example. We're going to, instead of using the standard library hash map, this is gonna do extern create hash map and then it's gonna use hash map, hash map. And in theory, this thing, we want to just work. We want this example to just work. We might put this as a documentation test later, but for now I just wanted it in its own file. So if you look at what this does, you create a new hash map, you can insert, there's a key and a value, they can be of different types. You can then later ask whether the map contains a given key, you can remove things by key, you can iterate over all of the things in the map or you can try to get something and get a none if it's not there and you can iterate over all the things in the map. You can also insert to replace and all of these things we want to implement. We want our hash map to do all of those things. So how are we gonna do that? Well, let's not have some tests to start with. We are going to need a struct hash map. That's where all of this is going to start. And the struct hash map, it's gonna be public of course. And inside of a hash map, the main thing we need is the list of buckets, right? So we're gonna have a buckets and it's gonna be a VEC of something. We gotta figure out what that is. It will be a VEC of bucket. So we're gonna have a struct bucket. What's gonna be in a bucket? Well, a bucket is itself a list of all of the key value pairs that hashed to that bucket. So this is gonna be something like items. And it's gonna be VEC of tuples of key and value, right? So this means that we need a key and value type here. Similarly, we're gonna need a key and value type here so that this can be generic over key and value. So this is saying that the user gets to specify what the key type is and what the value type is. In this particular instance, we don't actually need to have any bounds on key and value because VEC doesn't require its types to have any particular trait implementation in either this bucket. Of course, later we're gonna need things like the key has to be hashable. The key has to be comparable so that we can check whether the key that a user gives us is in fact in the map. But for the data structure itself, we don't actually need to do this. There's a really good guide, just the API guidelines, this thing. So this is really neat if you're writing your own library. Most of it is fairly intuitive, but for, where's the predictability? So this basically specifies lots of things. You should try to make sure that your library does in how its API works. And there's in fact one thing that ties into this, I think it's flexibility. So it says, maybe it's not here, that's annoying. It should arguably be added. There's been some debate about this, but the basic idea is that you want trait bounds to be only on the places where you implement the methods they use them rather than on the data structure itself. So we could say like this should implement hash and eek, but this means you can't now construct a hash map without having those bounds and everywhere that we implement something on the hash map, we would have to specify the bounds again, which is pretty annoying. There is an RFC that's landed to imply trait bounds like this, but we're just gonna ignore it for now. Ooh, two mics, let's see. That's interesting. What about if I do, is that better or worse? Like if I talk now, does this still sound underwater? Cause I think, I don't think there should be anything. Wait, let me see. Still sounds weird. All right, let me try something. So let's do this. How about now? Is that any better? Oh, that's weird. It might be that like one of the channels is weird. So what I'll do is when I upload the video, I'll make sure to only extract one channel. Still same. Oh, that's super weird. That is super weird. Yeah, this all looks right. Oh, weird. Is there a way for me to like? Ooh. How about now? It's not terrible. All right, the real question is whether you can hear me now or whether I just now turn off the... All right, we'll have to deal with that and I'll try to deal with it afterwards. As long as it's not too bad. Okay, let's see. So we have a hash map. So we could specify the trait bounds that we require for the types on the type itself, but usually it is nicer to specify the bounds only on the places where you need them rather than on the data structure. So it's actually a good question whether hash map does this. So I think hash map, yeah. So the struct itself does not have any bounds on K and D just like we don't, right? But the implementation is basically all of the implementation is requires that K is E can hash, right? So they've put only the trait bounds that are required in the appropriate places. And so that's what we're gonna do too. Now, the first thing you'll want for any map is you need a way to make a new one, right? So we're gonna have a pumpkin new that's gonna give you a self. And when you make a new hash map, of course it's gonna be a hash map. The buckets, this is where we start having to make decisions. So here, we need to start out with there being some number of buckets. So imagine that we get this hash of the key that the user is giving us and now we need to map it to a bucket. That means that we need to choose how many buckets there are because we don't want to dynamically grow this. That would mean that things shift around. So normally what we're going to do, I'm just gonna leave this here because it'll be easier to talk about. If you have a key, and I guess a value, I'll give back to why that is in a second. So basically this is gonna do something like key.hashmod self.buckets.lin, right? So this is gonna give us one of the buckets and this is also of course the way that we get collisions is because this value can be the same for different keys. But we need to decide on how many buckets we want initially. Now, usually the way libraries do this is just powers of two. So as long as you stick within powers of two, you're usually fine. There's also generally a trend in us to not allocate unless you have to. So creating a new empty map should basically be free and we're gonna do the same thing here. We're gonna say that if you create a new hash map, it is initially empty. It's only the first time you insert into it that we're gonna allocate. So initially there are no buckets. This of course also means that we're going to need a resize method, which is going to, so imagine that you have a map with only a single bucket. If you have only one bucket, then every object you have, every key you have is gonna map to the same bucket. So that bucket, because we're using a linked list, is just gonna get really, really long. And this means that looking up any key is gonna take proportional to the number of elements unless it's sorted, which we'll get back to later. But so basically that would be a very inefficient hash map. Whereas if you have lots of buckets, then now basically every key has its own bucket. So look up is just takes a single operation. So you don't need to scan anything. All the lists are at most of length one, but it means you're using lots of memory. So usually there's a trade off here. In general, what we often do is we, every time you want to insert and you feel like the map is too full, then you double its size. This gives you what's known as amortized costs over time because usually you don't have to resize. And it gets increasingly less likely that you will have to resize as you go. And this is what I think the standard library hash map does as well. So our resize method is gonna do something like target size is gonna depend on the number of buckets we currently have. We need a match here because if the length is zero, then we want some initial target size, probably something like 1024. But for our example, let's make it one. The reason I make it one here, for efficiency you should probably make it be larger because otherwise you're gonna do a lot of resizes in the small sizes. The reason I make it one is because for testing this library, we actually want to trigger enough resizes that we uncover any bugs that we might have. So we might change this later. In fact, we could easily do something like initial and buckets, u-size is gonna be one for now. So now this is gonna be initial and buckets. And for any other n, the target size is gonna be too big. Now, when you do a resize, exactly what that does, we're gonna do later. But suffice to say for now, initially when you make buckets, they're all empty, you can imagine that we're gonna have some number of buckets defined by buckets.len and each one of them is gonna be one of these bucket objects up here, right? So let's do insert first, because insert is probably the most interesting function that like if you can't insert anything into the map, the map is basically useless. So we already have something that looks sort of okay here. It turns out that there are a lot of ways you can hash things. This is maybe not very surprising. There's everything from sort of cryptographically secure hashing functions to very fast ones that generate a lot of duplicates. In our case, and we're gonna do the same thing as the standard library does, notice that it has its parametric over K and V and this S. So S here is a hasher and it defaults to this random state thing, which we will probably also want to do just for compatibility. So S is sort of a way to build a thing that can hash things is the best way to describe S. So if we look further down, you'll see that the requirement here for this is that S implements build hasher. And yeah. So here we want to implement this where S implements build hasher. And what build hasher does, so build hasher as the name implies lets us build a hasher. A hasher is this thing we looked at before that lets you hash something, right? And so this is what we're gonna have to do to our key. We're gonna pass it through one of these hashers and take the resulting value. And a build hasher is just a way for the user to specify how to get a hasher. So a build hasher could be something that constructs a SHA hash. It could be something that constructs any other type of hash. In our case, all we really need to do here is to call this build hasher method. So this means that our hash map is also gonna have a build hasher. Let's buff type S. So this is gonna be... Ooh, it's a good question. What do they do here? So for new, it gives you a random state. So new is gonna give us a... Okay, so what we're saying here is that if you create a new hash map, if you just don't specify what this is, we're gonna give you some sensible value for us. So that's why here we're not actually gonna be generic or S. Instead, we're always gonna give you a random state back. Random state is just a particular type of build hasher that comes in the standard library. So it's just very convenient to use. It means that the user will often not need to specify it. So our build hasher in this case is going to be a random state. Does random state take any random state new? Great. And now, of course, the reason we do this is as you see in the standard library too, there's a with hasher where the user can specify their own. We're gonna just not specify that for now. In fact, let's make this a lot simpler. Let's just get rid of this and say that it's always random state. In fact, we could go even further and just say that the hasher is always a default hasher. We wanted to make this really straightforward. So notice that this is default hasher and a default hasher is just something that comes with the standard library that uses some relatively sensible hasher and we can just use that. That way the user doesn't need to know about this additional thing and it makes our code simpler. So for exposition, that makes our lives easier. In fact, then we don't even need to store it. You can just create a new one each time. Yeah, no, we need to create a new one each time. So the reason we need to do that is hashers are cumulative. So the first time you write something to it, that's gonna change the hash. Calling finish is gonna give you back the current value for the hash, but you can also keep writing. If you look at finish here, yeah, so this does not reset the hasher's internal state. So if you continue writing, you'll keep changing the hash. If you need a fresh hash value, you need to create a new hasher. The reason why the standard library has this build hasher thing is because they actually randomize hashes for any given map. This is to prevent various attacks. If you're interested, you should go read the documentation for random hasher. I don't think that's something that we need to cover here. This just makes our lives a lot simpler, so we're gonna do it. So here, insert, we're gonna need a hasher, and that hasher is gonna be a default hasher. Of course, default hasher comes from standard collections hashma, default hasher, great. All right, so we now have a hasher. Now, what do we do with it? Well, wow, that's not what I meant to do. So a default hasher gives us one of these, and what we're going to do with a hasher is we're gonna use the hash trait. So we're gonna require that our key, technically that's not needed for new, so here we're gonna require that the key implements hash. And that's standard hash. So where's, all right. So if you search for hash here, the hash trait, so hash just takes a ref self, so this is gonna be our key, and it takes a state which is a hasher. So in order to get the hash, we're going to do key.hash into the hasher, and then we're gonna say bucket is hasher.finish, which is gonna give us the hash for the given key, modulo the buckets. Modulo the number of buckets we have, and that's gonna be our index into buckets. Now we happen to know that this value bucket is less than or equal to, well, actually it's not needed. The compiler can figure that out. So this is gonna give us some value that's in the range of zero to bucket self-len, which means that the bucket is going to be self buckets bucket. So that's the bucket we're gonna insert into. Now here, all we really need to do is we're gonna take the bucket and we're gonna push conceptually, we're just gonna push the key value pair. Now one reason why this is not okay is imagine that someone has already inserted something with that exact same key. If we did this, there would now be two key value pairs for the same key, which is not okay. So what we actually need to do is we need to walk the list of buckets and see if anyone else has inserted the key that we're already inserting. So here we're gonna have to do something like let i is bucket dot, let's do bucket dot find. Yeah, let's do this this way. Let's do bucket dot find. Find takes a ref mute to the key. So this is gonna be an existing key and this is gonna be an existing value. So if let some, let me write this out and then I'll explain what it does. E key ref mute value is find E key is equal to key. We'll see when we get to implementing get, but is that a problem for retrieving of the bucket count changes? Yeah, so if the bucket count changes, then we actually need to recompute all of these. So where resize is actually really expensive in a hash map. Now there are some tricks you can pull. So for example, the standard library hash map, instead of just doing modulo, it actually uses the, let me think, the low bits of the hash. And that way, when you resize, you only need to move half of the elements. So if there are twice as many buckets, you use one fewer bits of the hash. So only the things that had a one in that position would have to move. All the other ones stay in their current place. Whereas for our case, we will actually have to iterate over all the key values and recollect all the buckets, which is going to be fairly expensive. And that's why you want resizes to be rare, which is why we resize every, we double the size every time we resize so that in general, we will not need to do it. All right, so what this does is we are iterating through the bucket. We're trying to find any element that has a key that matches the key the user gave us. So in this case, find gives you a, actually no, it will not give me this. It's going to be a little bit annoying actually. How about we not use find? How about we instead do, yeah, let's make this easier to follow. For e key, e value in the bucket, it's easier to follow. All right, so we're going to loop through all the elements in the bucket. And if the key that existed in the bucket, if any item in the key is equal to the key that the user gave us, then now we know that there's an existing value there and the semantics of insert in that case is to replace the value. There are a couple of ways you can do that. The easiest is use mem. And in our case, we want to mem replace the value that's currently in the map. So mem replace takes a mutable reference to something and a t, in fact, let's pull it up here. Here, replace. So mem replace takes a mutable t and a t and it gives you back the thing that's inside of here. That was inside of here and puts this thing in there. Is the sort of way to think about it. It's just, it's quite literally replaced and gives you back the old value, which is exactly what we want here. We want to replace the existing value with the old value and we're going to return some of this. So this is to tell the user that a value was replaced and tell them what the old value was. If we get through this entire for loop without returning, that means that the key does not exist. So we can now just push the key value pair that we wanted to add and we can return them because no value existed for that key. So that's our insert. We could also try to test this at some point. In fact, let's write a simple test for this. Just for own sanity. I'll test. Test use super insert. Now insert is going to create a new map. It's going to do map.insert foo 42. Great. Notice here that we are doing a quality checking on key. So key in addition to implementing hash also has to implement eek. Eek is the trait for comparison for equality. That sounds wrong. There we go. We need to use the trait. So we're going to need to use the hash or trait. So remember that in Rust, you can only use the methods of a trait if the trait has been imported, which is what we're doing here. Yeah. So the bucket here, we want to be a U size, but finish gives you a U 64. This is a little bit annoying actually. So the problem here is you get a hash that's very large. And I think we want to do the modulo in U 64 space. So that way we know that it won't overflow. So the alternative here, we could cast this to U size and then do the modulo in U size. The problem is what if U size is U 32? So you're on a 32 bit platform. If you did that, then the hash you got back might be just wrapped. And that might be fine in this case, but I think the nicer thing to do is this, because here there won't be any truncation of the hash. There'll just be a straight modulo. And then we cast the result to a U size, which basically it has to be possible to cast it to U size because it will be smaller than the length of the buckets. And the length of the buckets is a U size. So this, what else is it complain about? It complains that the bucket is not an iterator, which is true. So remember we have this bucket here that's a VEC. We could just make this a vector actually let's do that instead of having the bucket type. I don't think we need the bucket type for now. It might just make some things nicer. Basically it was complaining that here we have something, so bucket here is of type bucket or was of type bucket and bucket does not implement iterator and therefore you can't, or into iterators, so you can't actually iterate over it. Whereas we know that bucket is a VEC. And now that we changed the type for hash map, now this type is VEC, or technically this, which is iterable. And now it's saying one of seven possible tokens. Oh right, only traits may use parentheses, that's true. All right, so now it's saying can't compare K with K. Yeah, so E key here is a reference to a key, whereas key is an owned key, so to compare them we either do this or we do this. I think in this case we might as well do that. They're basically the same. It's also saying that expected new key found V. Oh, this is a rectmate. Yeah, so what we're doing here is we're destructuring the iterator. So when you iterate over, in this case, a mutable reference to a VEC, the items you get back are mutable references to each element of the VEC. And so that's why we have this refmute word, like destructuring that. So we're saying if we just did this, then X would be of type mutable reference to a tuple. And so we're sort of destructuring that into I want to get rid of the mutable reference, which is the same as saying dereference it. And then I want to deconstruct the tuple into the key, which I just want to read only reference to, and the value, which I want a mutable reference to. So like so, value moved here. Shouldn't need to do that explicitly. Maybe I need to do this, but that seems excessive. Weird, you don't actually know why it won't let me just do this, but fine. All right, so it compiles, it probably won't currently work because remember how we just left resizes to do? So initially all our buckets are just empty. So we can't actually push anything into any bucket. So it's probably time we do resize. The way we're gonna do this is in insert, if we detect that the map is more than so and so full, then we're gonna do a resize. So here, that means we're gonna have to track items, just how many things there are in the map. Because remember, we can't get that easily just from the bucket count, because it's gonna be many things in any given bucket. So we actually need to count this explicitly. It starts out as zero. And we're gonna say that if self.items is more than if self.buckets.isempty. So either if we haven't allocated any buckets yet, remember the new doesn't allocate. Or if the number of items is more than say, let's do this. So either if we have no buckets yet, or if the map is more than three quarters full, then we're gonna do a thing. Here, of course, we're gonna self.items is equals one. Then we are going to do self.resize. And so what this is gonna do is as the number of items in the map grows, initially we'll just fill the buckets. And then at some point we'll say, okay, enough. And then we'll double the number of buckets, move all the things around, and then we'll continue to that and expand inside the buckets that we have. And so now this means that resize will be called basically immediately the first time you do insert because we have no buckets. So the target size has been computed here, and now we have to do something. Well, first of all, we're gonna have to set up the new buckets, if you will. So new buckets is gonna be a VEC. It's going to initially just hold empty VECs. So every bucket is going to be empty, and we're going to want target size of them. Like so, consider giving new buckets a type. Shouldn't be this. It's gonna complain to me later, but let's just start off with this, because it's clearer. And then, and this is where the expensive part comes in, we're going to do for, actually we're gonna do, well, there are a couple of ways we can do this. I think what we're actually going to do is for key value in self.buckets.drain. So we're gonna get rid of all of the things that's in the current VEC, and we're going to put them into new buckets. So in order to put them into new buckets, we basically need to do the same thing as we did originally. We have to figure out what bucket they fit into. So here, for every item, we create a new hasher, we hash the key, then we stick it into the new buckets. So we find the bucket in the new number of buckets that we have, and then we do new buckets, bucket.push, and then we push the key in the bucket. So this is, we're iterating over all of the old key value pairs, or all of the key value pairs that are currently in the map, we're putting them into their new buckets. And because there are no duplicates here, we don't need to do the search. And then, now that we're done, we then do, let's use standard man at the top. Then we simply replace the self.buckets with new buckets, like so. So now we're going to take all of the, we're going to take the new buckets that now hold all the key value pairs because we've drained them from the old one. And we're going to swap that in place for the existing self.buckets. And now we have buckets. So now we have twice as many buckets and all of the old values have sort of been imported. And so now in theory, this should work. Let's see if it probably doesn't compile, right. So here's, this is a little bit annoying. This syntax for creating vectors requires that the type you give here is clone. Now we happen to know that a vector new is clone, but all Russ knows is that this is a vector of type kv, and k and v are not clone. And so that means that a vec kv is also not clone. And as you see, that's what it's complaining about here too. It's saying that k and v are not clone. And therefore I cannot use the syntax to create this many of this type because this type is not clone. Even though we know that in the specific case of vec new, it actually is clone. Playing with Rust, that has been super annoying. Which part that it doesn't know that it's clone? Well, I mean, it's actually really complicated for it to know that this thing contains no data. So I'm not really blaming Rust here. It's just like, don't have enough stuff in the type system to say that this is okay. So the way we have to work around this is we create it with capacity. So when you create a vec with capacity, it basically allocates that much size immediately. And then we do new buckets.extend zero to target size.map. And we don't care about the value, but we create it. So what this is doing is it's iterating, it's just creating target size new empty vex. So this doesn't allocate, so this should be fairly efficient, but it does have to walk all the buckets and create a vec for each one, like push each one to the end, but none of this should need to allocate because we did the allocation up here. So this is just like a bunch of CPU operations that are basically unnecessary, but we need to have them there in order to make the compiler happy. Let's see if it's happy now. Expected struct vec found tuple. Oh yeah, this gives us buckets. So really what we want is a flat map. So this gives us a bucket and we want bucket build drain. So we want to drain every bucket. Why not let mutant? So your proposal is to do, I don't think that will work either. I could be wrong. Yeah, this also requires clone. It's basically the same, right? This is the same as that. So it still has the requirement that the, because you're telling it that you want like this many of this type, but this type is not clone and therefore it isn't able to. What does dot dot mean? Yeah, so when you do a drain, when you do a drain on a vector, you give it a range and you say, I want to drain these elements and only these elements. So for example, here I could say like zero to three and that would drain only the first three elements. In this case, I want to drain all of them. So the reason we have to use this double drain here is because we're iterating over all of the buckets. See, this could just be iterative. We're walking all of the buckets and for each bucket, we want to drain all of its elements, right? We want to take all the key value pairs and that's basically what this does. Let's see. Oh, right, iterative is on the parameters. Great. So we create new buckets. We remove all the key value pairs from the old buckets, put them in the new one and then finally swap the two buckets. And now if you run cargo test, this is going to work. The map doesn't need to be mutable. That is true, but expected item. What? Oh, it's trying to run the examples. Great. Okay, so our insert works. So we don't actually know whether our insert works but it doesn't crash our program. So let's start. So in order to test whether our insert works, we're also going to have to see whether it get works. So get takes a ref self and a key. Let's get back to what this type is and it gives you an option, right? So this is going to say, we're going to do a look up into self and if it exists then we're going to give you back a reference to the value, otherwise you get none. And the implementation of get is actually really straightforward. Actually, let's do this. Let's have a convenience method for is a, sure, let's do this for now. Okay, let me, this is not actually what this type is going to be, but let's use that for now because it's basically right. This is going to give you back a U size. And I just wanted to encapsulate like this code into its own little snippet, tk, great. So now this can just do bucket is self.t, actually self.t of the key. Yeah, so we're using, so forget we just want, we don't need the user to have to give us a K because that means that imagine the K is like a, like a long string or something. We don't want the user to have to own that string. We don't want them to have to like, if they only have a reference to that string to clone it or something. So we only need reference to it. The reason I'm a little bit hesitant is because imagine that you have a map where the keys are strings as in capital S string. You want to be able to index into it with like a star, like a constant string. And this API won't currently let do that. There's a way to get around that though that we'll look at later. Let bucket two times won't give you an, won't that give you an already defined error? No, it actually won't. So Rust does let you override names, which is actually really convenient. Like I like being able to do this. In this particular instance is not terribly important, but it's sort of nice to be able to remove an old name from consideration to override it with, from this point on you should like, only deal with this type and the, or deal with this value and you should ignore the old one. The reason this is kind of nice is because the type system mostly saves you from screwing up, right? So the types of these are very different. So if I meant to use this, but I got this, it is unlikely that this type is just gonna work where I was expecting this, right? It just creates a new variable. So the old variable is being shadowed by the new one. So anytime you use this name, you're now referring to what this name points to rather to what this name points to. All right, get. So get is very straightforward. We're gonna get a bucket by calling self.bucket with the key. Then we are going to essentially search the, search the bucket that the key has to. And here we can use find. So find is gonna be getter. Find is given a reference to the things we're iterating over. Think in this case it'll actually be this. It's just given a reference to the things that you, no, actually, no, find is just given the items. Okay, so iter gives us references to the contents of the bucket. So reference to every item in the bucket. And every item, of course, we know is an existing key and an existing value. We don't care about the value for find. And all we care about, we want to find the thing where the E key is equal to the key. And what find does is, if it doesn't find anything where the condition is true, then it returns none. Otherwise it returns some and this thing, right? But because we want to return just the value and not also the key because the user already has the key, we can map this and we ignore the key and we give just back the value. Yeah, so I mean, you mentioned underscore JS. This is sort of the functional programming paradigm, sort of, right? And Brussels lets you mix these so you can sort of, you can choose the one that fits the situation the best. So for insert here, technically we could express this for loop in a purely functional way. I shouldn't use the word pure, but you know what I mean. But in this case it's just like easier to deal with this as a for loop. We might later want to, this for loop is pretty expensive and same as this find. So we might want to keep the buckets in sorted order, for example, so we do a binary search, but that's something we could leave for later. Okay, so we have a get. So in theory we should now be able to do this and then we should assert equal that map.get foo is some 42. Unexpected end of macro invocation. Yes, indeed. Like so. So this takes a reference to the key and not the key type itself. So the key type here is refster. And so we need to give a ref to the key type which is a ref refster. Hey, it passed. Okay, so we are now able to insert into the map and then do a get and we get the thing back. Can you explain ref, yeah, yeah, yeah, yeah. Okay, so you want these two lines. All right, sorry, these two. Okay, so let's look up iterator because that's gonna help. So iterator in this case, right, we're getting an iterator over the contents of a given bucket. So remember that the every bucket is one of these, right? So every bucket is a vector over tuples of key value. So when we call iter on a given bucket, what that's gonna give us back is it's gonna give us an iterator where the item is a ref to a KV. So that's what dot iter gives us back. On that iterator, we have chosen call find. So find takes a predicate closure. So the predicate is just a closure that is given a reference to the item and returns a boolean, whether it matches or whether it does not. In this case, it is given, ignore this for a second. It's given an X where the X is of type reference to this and then it should return true or false. In our case, the only way we can tell true or false is by comparing the keys. So we need to extract the key from X, right? So because it's a tuple of K and V. So one thing we could do here is we could do X dot zero. This would do the same thing. However, we don't really want to do that. It's hard to read, right? We would need to remember that it's like the first element and it's a little awkward what's going on. So the way we can do this is we can deconstruct. So in all rust functions, you can deconstruct arguments directly. So think of this as sort of like doing a match. It's doing a match on X and then using this pattern. This pattern, right? And it's basically saying that I know that this type is this pattern. So in this case, there would be no other thing that it can be. So what we're doing here is we're saying that we're telling find that you're going to get items that look like this. And what I want you to do is I want you to bind E key to be a reference to the K and I want you to not bind anything to V. And now we can refer to E key directly. And we're doing the same thing here in the map. Does that roughly make sense? Let's see. Rust feels complicated. Yeah, so remember I could write this fully imperatively, right? So I could do the following for E key, E val in self dot buckets bucket. So these two segments are basically identical, right? So this and this are the same. Sorry, this should be rough. In fact, if you want it to make it even more like explicit, you don't want to use the pattern matching. Maybe X, X dot zero is key then X dot. So this is with no functional stuff at all and not using pattern matching, then you would get this code. This code down here is the same code. It ends up doing the same thing. I happen to find this easier to read, but you can do either. And that's one of the things that's nice about Rust. That like this pattern matching is really, really powerful in part because you can do it in so many places. Like I can do it here, right? That's just really, really neat. But the, I find this easier to work with. And in fact, we can do this. Great. Probably unnecessary, don't really matter. Right, so we now have a map where we can do a insert and we can do a get. Is that pattern matching or destructuring? It's destructuring, but destructuring uses pattern matching. So you should think of destructuring as pattern matching where there's only one possible pattern. And usually if you pattern match it's because there are multiple, like if you pattern match over an enum, for example, there are multiple different ways in which you could match. Whereas if you're doing destructuring, you're saying that it's specific type that I want to destructuring in this way. And the compiler will check that that is the only type of pattern you can even receive. All right, so we're missing remove. So let's add remove in here. So remove takes a mute self and a reference to a key. And also gives you back an option because it might not remove anything and it's an option V. Now, this is very similar to get. Like so. It is a little bit different though in that it should remove the item if it's there. So here, there are a couple of ways we can do this. So there are many ways we can write remove. I'm going to do it the following way. Is that part? Yeah, great. So we're going to use retain. So retain is also a really useful method on vectors. So let's go up here. So on a vector, you have the retain method. So retain will walk the vector and for every item calls the closure you give it. And if the closure returns true, then the element is left in the vector. If it returns false and the element is removed. And the reason you want to do this is because it turns out that if you use retain, you can be slightly more efficient about how you choose to remove things if you remove multiple things. Actually, in this case, we know we're always removing it most once. So let's do this in an even better way. We need to find the index of the key. So we're going to do self.buckets.bucket.position. Position is sort of like find, except that it returns the index rather than the value. So find here returned the value that we found. Position returns the index of that value in the iterator. So here we're going to check whether E key is equal to key. And we use question mark here now, right? So the question mark operator is really neat. If you haven't used it before, it basically says that it was initially intended for result. So if you have something that returns a result and then you call a function that returns a result, then question mark is equivalent to the following. If you have something, question mark, it is the same as match something. It is okay, if I do, yeah. Okay, K is K, error E is return error E. Well, let's ignore the into. It basically does this. So if something is okay, then it unwraps the value. Otherwise it returns from the current function with the error. Now it turns out you can also do this for option. So it looks like this. And that's what we're doing here. We're saying that if position returns none, then just return none immediately. Otherwise unwrap the value. And so we can use that very nicely right here. And now what we can do is because I, so if we actually get an I back here, that means we didn't return from the question mark. So the key does in fact exist. And then we can just do self buckets bucket, store a reference to that bucket. Yeah, option works to the question mark was an option. I think it's unstable where I guess it's about to find out. And then we can use the swap remove function. So swap remove, so swap remove replaces the last element in the vector with the one you are removing. And the reason you want to do that is otherwise you have to shift all the things in between. So if you have a vector that's like A, B, C, if you now remove B, then you would have to move. So if you remove this, in order for the vector to not have holes, you would need to shift C to the left like this. And of course, if this vector is long and if you now remove B, you have to shift C, D, E and F to the left. What swap remove does is it swaps B with F and then it removes B and now you just truncate the end. And so it's a lot more efficient. It doesn't have to do many more operations, but it does change the order of things in the array. Currently that's okay because our buckets are not ordered. Yep, that's exactly right. So the question mark will return from the function if it gets a none and if it gets a sum, it will unwrap it. And so I here is of how to use this. Whereas position returns an option uses. Swap remove dot one because we want just the value. All right, so now we can test this further. We can assert that remove gives us back 42 and we can assert that get again now gives us none. Hey, we have a working hash map with remove. Oh, RustVector, the RustVector has a lot. So the RustVector is a very well-documented primitive in part because they know there will be used really at a really low level. So if you see this guarantees section on a vector, it talks about like exactly the guarantees that the implementation gives you so that you can rely a lot on the underlying implementation details because you often need this for building more advanced data structures. Okay, so now we have map. We will want a couple of other things. In particular, we will want things like Len, which gives you the number of items. Oh, remove should here do self items minus equal one. That's good. And we want product then is empty. Takes it up self returns a bool. Self items is equal to zero. And now we can add that to here. So here we want a certain map Len is zero. We want to assert that it is empty. After we insert, of course, it should have one should not be empty after remove. It should be empty again. Great. We could also run Clippy on this in theory. Slightly install force. Run on the background. So Clippy is a really nice tool for just like checking that your code is okay. Let's look at the example now. So the example now does insert, it does contains key. Okay, so contains key is basically a get except it returns a bool. So contains key. It's just going to be find.is. Not just off return a bool. I go T, what else does it need? Oh, right. This needs to be fn main. Let's see what our example does. Let's see. Okay, so there were a couple of things that the example from the standard library needs from us that our hash map does not currently provide. Let's deal with them in reverse order. So the first one is you want to be able to iterate over all the elements in the map. So that's what they do here. They iterate over all the things in the map. Currently we have no way of doing that. And so we will fix this. We basically want to implement into iterator for that for any kv, kv into iter itself into a something. I haven't quite decided yet. You work for a face punch. No, I do not work for face punch. I also don't know what face punch is. Oh, standard library just as self get is some. I feel like this is more efficient, but I might be wrong. I can feel like this might optimize better, but I'm clear. I feel like it could, yeah, I mean, that's fine. We can do that. We don't really care. Sure, it's even easier. Okay, so what we really want is we want some way for the user to iterate over all the values in our map. There are many ways you could do that. So you could imagine something like drain where you remove all the key value pairs and yield them one at a time. So that's what drain does. And that consumes self. It consumes the hash map because you're giving away all the keys and values. The other thing you might want to do is you want to iterate over all the things in the key in the map. You don't want to get rid of them. You just want to walk them. And the way to do this, like if you want the user to be able to write code like this, like for in something, then if you do this, it usually means drain. So for this, so for loops in general, require the thing after in to implement into iterator. So which is a trait that just says that this thing can be turned into an iterator of some type. And so if you wanted to be able to do this, now you would get back owned copies of the book and the review. If you do this, you're saying, I want a reference to this thing to implement into iterator, which usually implies that the things you get back are here actually references to the key and references to the value. So this would be the types. In this case, if you remove this, then this would usually be like that. In our case, because the example just uses the reference, we will just implement iterator into iterator for a reference to our map, which should make this example work. So this has two types, two associated types. It has item, which in our case is gonna be a reference to the key and a reference to the value. And it has an into iter, I think it's into iter, which is the type of the iterator. So remember that if you call dot into iter on this, you need to get something back. Like it has to be some kind of type. And so into iterator is what that type is. We might want to be, actually, I don't know if we can use the inflator iterator here. See if this works, that would be really nice. But I don't know if it's true. Yeah, so the into iterator trait only has a single method and that is into iter and it needs to return itself to iterator. Yeah, it's not stable yet, that's too bad. Okay, so we're gonna return like a iter. So we're gonna need some pub struct iter over K and B. We don't know what's in it yet. And that's what we're gonna return from into iter. So that's just gonna be iter and use, so this iter KBU is gonna take a ref to a hash map over K and B and get back to self. And then we're gonna have to implement iterator for that type. For this type, the type of the item is gonna be the same as here. We're gonna give back references to the key and references to the value. And the requirement for iterator is that you implement next, which returns an option self item. So that is it returns some of an item if there are more things to iterate over and none if there are no elements. Ooh, more or less, so much chat. Let's see. Oh, yeah, no, this is not Rust the game but Rust the programming language. And I do not work for the makers of that game. Yeah, Twitch is geared a lot towards gaming in general. So finding programming languages that are called the same as games is not super easy. Let's see, why are lifetimes needed here? Yeah, so okay, I just wrote a lot of code. So let's walk through what I wrote. The reason I wrote all of this in one step without going through it in too much detail is it's useful to have the code so that we can talk to it as a whole. So remember the general flow here is that the user has a reference to a hash map and they want to iterate over it. That's what the into iterator trait is for. It is going to call the into iter method on the, so it's going to call this method on this thing that the, yeah, this thing that the user has and that's going to give it back some type and that type is going to be this type and that type, we're sort of promising an iterator that this type implements iterator where the item is this. The reason the lifetimes are needed is because the key and value references that we return in the iterator are tied to the lifetime of the map. If we did not do this, so if we just had this, first of all it wouldn't compile, but second of all, if this did indeed compile, the reason it would be a problem is because imagine that the user like does iter is bookreviews.idder.next.unwrap. So this gives them the first item of the iterator and then they drop bookreview, but now they still have iter because iter was just a reference but it didn't have a lifetime that was associated with the hash map in any way. So without those, this iter would continue to live. It would be pointing to stuff that has already been dropped and so that's why that's not okay. That's why we're saying here that the items you get back are tied to the lifetime of the reference to the map. So if the map goes away, the items are no longer valid and similarly the items cannot outlive the map. So the map needs to continue living for as long as you want this iterator to work. The audio issue mentioned early in the stream just suddenly fixed itself. Well, that's good. Oh, that's so weird. It is really humid here so maybe the microphone got sad. Oh no, it's weird. Yeah, I do have a GitHub. My GitHub is this one if you're curious about things. That's me with less beard. Let's see. So, yeah, does that roughly make sense why we have the lifetimes there? That's true, we could cut the new. That's fine, fine, fine, fine. Well, I sort of like the new but okay, fine, I'll cut the new. Okay, so the iterator is gonna need to have a reference to the hash map and it's going to have to keep track of what bucket it's currently at and where inside of that bucket it is at. So remember, each bucket might contain multiple key value pairs. And so next is going to be oh, it's a good question. Next is going to do the following. The item it's gonna yield is going to be self.map.buckets, self.bucket, self.at, right? So that's basically what it's going to do. What we'll do is we'll do use get map b. So I know this is like very functional style. So what we're saying is, no, I want this slightly differently. So the trick here is we need to, if we reach the end of a bucket, we need to move to the next bucket. If we reach the end of the map, then we need to return none and continue to return none. So let's map on self, map buckets, get self.bucket. If this gives some, then the question is now whether we can, whether the bucket contain self.at. If that's none, that means we've spilled over the end of the bucket. If that's some, then we can yield that element. And this of course is a key value pair. So we return v. Except we'll have to move along self.at and self. Do you plan what you're writing or are you thinking of it right now? I'm thinking of it as we go. That's partially why the code moves around a lot. But I have implemented hash maps before. So that probably helps. Okay, let's see. So if let's deal with the most straightforward case first. This is going to be bucket zero at zero. If we get an element, then what we really want to do is we just want to return sum of v. However, we also need to bump self.at. So we're going to say that we're going to return the current element but the next time you call next, we want to get the next one. So we're going to increment that. Now we don't need to do anything about bucket. We don't need to like check whether at is inbound because if the next time we call next then at is out of range for that bucket, then this match is going to return none. So in this case, here this will, this indicates that we reached the end of the bucket. And so we will do self.bucket plus equals one and self.at equals zero. So this is saying we reached the end of one bucket now start from the beginning of the next one. If on the other hand, the bucket is out of range. So we have reached a bucket that is not in the list of buckets. Then now there's just no way that there are more items. And so in this case, we can return none. Now notice that here we sort of need to continue. So one thing we could do here is we could do self.return self.next. While this would be nice, that's not actually okay because Rust doesn't have a guaranteed tail call elimination. So this might be like self.next, call self.next. Imagine you have a very sparse map. So the map is like all the buckets are empty except the very last one. You're gonna keep calling self.next inside of itself. So your stack is gonna keep growing. And then you're just gonna run out of memory because you're walking all the buckets but calling a function each time. With tail call elimination, this would go away. The way we're gonna get around here is we're gonna flatten that by making this a loop. And then say that in this case, we break with none. In this case, we break with some. And in this case, we continue. Loops can now return values, which is really neat. A lot of people don't know this, I think. That you can break with a value and that will be the value yielded by the loop. I think storing outer inner. Yeah, so Rust doesn't, it sometimes adds tail calls but it does not guarantee that it adds tail calls. There's been some discussion, I don't know what the current state of it is of introducing a new kind of return keyword that you could do like instead of return self.next here, you could do like become self.next, which is like it would be a compile error if it could not be tail recursive. But for now, we'll just flatten it with a loop. It's fine. Yeah, you could probably instead of storing these U-sizes, we could probably store direct iterators into the bucket but this code is fairly straightforward anyway. But so this is roughly makes sense. So the way we're gonna iterate is we're gonna check whether the index into the bucket that we're currently pointing at is valid. And if it is, then we increment the index into the bucket and then we yield that value. Otherwise, we move to the next value and try again or move to the next bucket and try again. Yeah, there's, oh, I should have find this, the tail call, this one, this one. Yeah, there's been a lot of discussion on this. Proper tail calls, sounds nice. Wait, why does this link not work? Apparently that's been closed. Probably has postponed. Yeah, okay, so it's just been postponed for who knows how long. But yeah, there's been discussion of it. There's just nothing has been settled quite yet. As Ganker pointed out as well, like it gets a little bit tricky and rust because of destructors. Like when do you call them if you're recursing and it's a little tricky? Even more proper tail calls, that's true. Okay, so we have in theory an iterator that's complaining the K might not live long enough. This is another thing that's gonna go away eventually with implied trade pounds. Basically we're saying that not only is the pointer to the map going to live long enough, but the values in the map also live long enough for the iterator to work. Yeah, because our iterator yields both the key and the value. And so therefore we don't wanna yield just the value. The reason we destructure nonetheless is because otherwise the item would be this, which is a little bit more annoying for the user to work with. Like if we just did like this, well I guess that, then this would be the item type. And this item type is a little bit more annoying for the user to deal with because they have to destructure and use refs. So if we instead do this, then we get the nice return or the yielded item type of two references. All right, and this complains now because it needs the map, which is true. Ooh, so one optimization we could add here. I don't think I care enough as we could count how many things we've yielded. And if that matches the number of things we know are in the map, we could stop yielding more items. That this means you don't end up iterating over the trailing set of buckets. But assuming the hash function is uniform, this should not matter all that much. Yeah, that's a good point. So Ganker just pointed out that if you do, if your return type is this, then now you require that internally you store tuples of KV because otherwise you couldn't produce references to it. Whereas this is just saying, I have a reference to a K and I have a reference to V and you're not guaranteeing that they're stored together in any way. That's a good point. You want to count any way to provide size hint. That's true. I'm gonna probably not implement size hint for now. But maybe later. We might get to it. You might get to it. Let's see. Okay, so that works. Well, I guess we don't actually know whether it works, but let's write a simple test for it. Iter. So we're going to insert some values. Foo, bar, buzz. What's the last one? Like clocks? I feel like that's the four KV in map much K, much actually star K. If it's foo, then the sort of the E is, actually let's just de-structure there. That should be 42. Technically, this test is not correct because it could be that you never get any of these values or that you get some of them multiple times, which it technically should check. But what we're going to do instead is just dot into Iter, dot count. So this is a somewhat weak test, but it will basically get the job done. Test just the lib. Okay, so it works. So at least we know that it generates, we know that the Iterator indeed generates four items and we know that those four items, the key value mapping is correct, even though it could be they're all foo. We don't actually test for this, but let's just assume it's right. Oh yeah, the unreachable macro is really nice. There's unreachable and unimplemented. They all just panic, but they're just nice semantic markers. They panic with slightly different messages, I think is about it. Oh, it's raining. All right, so what are we now missing for the example? Okay, so notice here that they have a, any other neat macros probably. So the ones I think I use the most are just, that's not what I want, macros. Yeah, so the debug assert macros are the same as assertions, but they are compiled away in release mode. So those are kinda nice. Eprint and Eprint line prints the standard error. There isn't a can never happen, but that's basically what unreachable is, right? One thing I have wanted is incomplete. So it's something that will only fail to compile if everything else compiles, but it does not exist yet. Include string is pretty useful. But yeah, I think unimplemented and unreachable are probably the most nice. All right, so yeah, so the thing here, if we look at the example again, notice that they insert using strings and the values are strings. This means that this hash map, yeah, as they point out here, this hash map has a key type of refster and a value of restar. And then they try to do contains key and give just a refster again. But notice that contains key take a reference to the key, which would technically be like a ref, refster, right? That is the value we're expecting. So this would compile just fine if I did this. All right, we can test it. Yep, so that runs just fine. Examples, cargo R, example std1. Great, so that runs just fine. But we don't really want the user to have to give this extra ref. And so that's where this really neat and also somewhat confusing trait called borrow comes in. So borrow, let's do the crate level documentation first. So borrow is a little bit of a, it can be a little bit confusing. They've done a lot of work on the documentation. So it has gotten better. They also have an example that is basically HashMap. So I'll basically be saying what's here. But this also means that you can read it on your own time if you want to. The observation is that contains key get and remove. We don't want them to take a refk. We want them to take a queue where, let me just write this out first. And then I will explain what it does. All right, must this syntax? Okay, so we want to get to take a reference to anything that's a queue where k can be, the way to read this is where k can be borrowed as queue. So borrowed as means if you have a reference to one, you can get a reference to the other without any conversion. It's just, if you have a reference to one, that is also a reference to the other. And then we require that queue is hash and eek. And because borrow implies that the reference is to the same thing, if queue is hash and eek, that hash and eek must be the same as the k hash and eek. So borrow is basically saying that these two types can both be, if you have reference to one or the other, they can both be considered the same type of reference. And the thing pointed to has the same semantic meaning. In this case, what this means is sort of that if you have a queue, you can use it as a k. Or rather, if you have a k, if you have a reference to a k, you can treat it as a reference to a queue and it will be the same. We also say that queue doesn't need to be sized because the key, we're just taking a reference to it so there's no reason for it to necessarily be sized. I don't think this matters all that much. But it's in the standard libraries so we might as well use it. I think it's so that queue can be stirred. This way, queue can be stirred, which is not normally sized. Okay, so in this case, this is gonna change a few things because now a bucket needs to be the same. I'm just gonna take a queue. Oh, and array T, that's a good point. Yeah, so this basically guarantees that the key, if you hash the queue, it gives you the same hash value as if you took the corresponding k and hashed it, which is why this should then still work. Oh, I need to use borrow, borrow. I want auto imports, especially from the standard library. Right, so now the trick here is to say that the e-key, so remember e-key here is a ref k and what the key we have is a ref queue. So in order to compare them, we're gonna have to borrow the k as a queue, which is what this would do. Similarly, we can now add the same trait bounds to contains key. We can add the same trait bounds to remove. And now here, of course, as well, we will also have to borrow. And lo and behold, the example now runs exactly like it does in the standard library. So the exact same examples now work on our hash map. Hash map is worse in a lot of ways, but it has the same API, it provides the same features. So this is pretty nice. This is like a good starting point. The next thing I think we wanna add is the entry API. Let me commit this first somewhere. Support, first hash map is like this. Turns out it's hard to type things when they're off screen. So we're gonna make a new repo so that all of you can see this rust basic. Let's build a hash map. Probably not the most useful thing, but hey. So what we have now, now this code should all be pushed here. So if you want to see the code, you can now go browse it here and it should all work fine. Great. Now let's look at entry. Okay, so the entry API is really neat. If you haven't seen it before, you will be very happy. So the idea with the entry API is basically that you can get a reference to where something will be inserted into in the map. So the way to look at this is here. So entry, entry, well, just do not collapse. Okay, so entry takes a mute self and a key and gives you back an entry. And entry is an enum of either occupied so there exists something for that key already or vacant, there does not exist something for this key already. On an occupied, you can choose to replace it or you can choose to get the key that's currently there. With vacant, you can choose to insert. But the sort of crux of this is you have this or these or insert methods, which let you do things like, that's the best example of this. Imagine that you're keeping a counter of some sort. So you've probably written Python code that looks a little bit like this for X in X's. If X not in, so you have some like collect is equal to sum. If X not in collect, then collect X equals this and then collect X dot push, right? This is code that everyone has written or something that's like similar to this, right? You would push something else, but like code similar to this where like, you have to initialize the things in your map somehow. This is what entry is perfect for. So entry is an API that lets you do the following. Book reviews dot entry, my book, dot or insert with, back new. So this means if there is no entry for my book, then put a vector there. If there is, then just give me the vector. This returns you a mutable reference to the value, so to the vector, so you can do this, right? So this is, if my book did not exist before, it will insert a new vector and push 42. If it did exist, it will just push 42. This is a really neat API for working with collections. And so what we're gonna do is we're gonna implement that. So we're first gonna, we're gonna do the same thing we did initially. We're gonna take the example from up here, which basically just does a bunch of stuff with that. Let's turn create hash map, use this, FNA. Great. So the basics of the entry API is that you have a pub FN entry, gives you a mute self and a key, and returns an entry over KND. And we're gonna have to write some code here. How do you know when to create new files? So in this case, currently, I'm just keeping the entire hash map in one file because it's not all that large. I think the entry API is actually in its own file internally, but I don't remember. That's pretty unhelpful. I think entry is in its own file in the standard library. In general, I do it once I need to scroll lots in the file. Entry's all in line? Okay, yeah. But yeah, I like to try to keep files contained by function mostly, not in the sense of function, but by their functionality. So in this case, hash map is a fairly self-contained thing. And so I like to keep all of that in one file. Sometimes I will extract out particularly hairy things or things that require unsafe, but it doesn't really matter all that much. Okay, so we're gonna have a new enum called entry. It is generic over KND. And just like the real map, we need to be able to track the difference between an occupied, which is gonna have something and a vacant, which is gonna have a something. Specifically, we need a Pub struct occupied entry, KV, and a vacant entry. The reason we need these to be different is because you can do different things with an occupied entry versus a vacant entry, right? They just have different things you can do to them. So an occupied entry, all an occupied entry really needs is it needs, so it's gonna generate over lifetime, it's gonna have a mutable reference to the entry in the vector. Now, I'll get back to in a second why that's not enough, but for now, let's just look at this. So element, okay. Right, so it's gonna have a pointer to the currently occupied entry. So the thing that's in one of the items of the bucket. A vacant entry, on the other hand, all it really needs to do is it needs to have a reference to the bucket that it's going to insert into. Now, this is gonna also keep the key around because when it eventually pushes, it's gonna have to push that key. The occupied entry does not need to do that because the element already contains the key. Yeah, I've noticed that. So lib collections usually has big files that are relatively self-contained, except for things that are internals or unsafe. So occupied is gonna hold an occupied entry, KV. Of course, all of this is tied to lifetime of the hash map because you can't start messing with an entry after the map has gone away, for example, vacant entry, a KV. Right, and now the place where all the convenience comes in, if you have a entry, KV, then you could do, let's just do or insert for now. Is that all the example uses? I use this both, okay. Or insert takes a consumed self and takes a value. So the value that you want to reserve wants to insert and then gives you back a mutable reference to that value. So the way to think about this is that or insert, if self points to an occupied entry, then it will just give you a reference to the occupied, so to the V that's inside of the element that already existed. Otherwise, if it's a vacant, it will put that value into the map and then give you a reference back to that same value. So in this case, we're gonna map self. We're gonna say that if we are an entry occupied, don't do that. So if we're an entry occupied, we're gonna have to do one thing and if we're a vacant, then we're gonna have to do something else. If we're occupied, we don't actually need to do anything with the value. We can just throw it away. Specifically, we can just do E dot element dot one. We just give a reference to the value that's already there. If it is vacant, on the other hand, then we need to insert the entry. So we will do, we'll actually destructure, gee, I guess we can just do E dot insert. And then we'll implement, because you can imagine the user wants to have finer-grained control and start interacting with these types directly. And so therefore, we'll implement an insert. Insert here, and this also needs to be pub. So the insert here also just consumes self and takes a value V, returns a V. So this is gonna self dot bucket dot push. Self dot key and value. And then last. Is there a last? I don't remember. Does a VEC have a last? It has a first. Last me, great. Dot map, we'll actually dot unwrap. Why should you add ticke in the input block? Isn't the ticke in the type enough for the compiler? No, so we need to say that we are sort of generic over any lifetime. Because if we did this, it wouldn't know which lifetime this ticke is. So for example, the example of this would be static. This is valid. Static here is not generic. If I did this, which you should probably never do, static is now not, it does not refer to static or refers to some lifetime that is provided for the input block. That's why we need to give ticke. What does the dot one do? So remember that our buckets keeps key value pairs. We want to return a reference to the value. So the dot one gets the value out of the key value tuple. So I'm taking a mutable reference to the value. The reason this unwrap here is safe is because we have just pushed to it. So we know that's our last element. Great, so that's what or insert does. Or insert with is very, very similar. It takes, except that it takes an F. It takes like a maker F, where F is an FN once that returns a V. So the idea here is that imagine that you have some, something that's expensive to create. So imagine you had a vector that actually did a bunch of allocation. You, with the code that we've written so far, oh, did I move it? If you have like book reviews, dot entry foo dot or insert Vec new, then this means that you are always allocating a new vector regardless of whether or not foo exists. Whereas or insert with, you would not call Vec new, but Vec new would be called for you if foo does not exist. So you avoid doing the work of constructing the item unless you need it. And so that's what this would do. This would here be maker. So that's or insert with, and then one thing that's, I don't think it's stabilized yet, but it's really convenient is or default, which does not take any of these, but it requires V to implement default. Then now, if you had a map where the values are Vec, for example, then you could just do entry foo dot or default, and that implies that you want or insert with Vec new. This is in the standard library in Knightly, but I don't think it's stabilized yet. So I think this will currently work with the example, maybe. I guess we're about to find out. Oh, this is the buckets full tuples. May not live long enough. Yeah, so this is, you need to guarantee that the keys and the values won't go away there. It's a little sad, means we have to do this. Oh, sorry, this has to be key. And now 39, this we need to guarantee that it lives long enough. Great. Okay, so now for the entry method itself. The entry method here is a little bit finicky. So what we want to do is first, we want to get the bucket for the key. So that's all fine. And now we want to match. We want to try to see whether we can find the value, right? So remember, we need to return a different kind of entry depending on whether or not the element exists. So we will do bucket dot find. So where's the thing where we do find here? Yep. If that finds a, this is gonna be a mute this time. If it finds a bucket entry, then we do one thing. If it does not, we have to do something else. Here, my guess is lifetimes are gonna bite us, which is a little bit sad, but we might be okay. Yeah, it's gonna, we're gonna have to... Actually, let me write this out, show you the error, and then tell you how to fix it. So let's see. So if we do and find an entry that matches here, then we want to return an entry occupied and then an occupied entry where the, what did we call it, item element? Element. So the element is gonna be that entry. So I guess we could probably call this element. No, let's instead make it entry. Entry is good, right? If it's none, so this means we didn't find something with the key for the entry, then we return an entry vacant. And it's gonna be a vacant entry. And the vacant entry has a reference to the bucket and it contains the key. So this will have key and bucket. So now you might think that this is all good, but my guess is the bar checker will yell at us. Right, this is now called entry. This is now called entry. Anything else? Two. Do I really need to do that? Can't compare K with K. Oh, here we have a key of type K. If we don't have this Q trick, so we don't need to borrow. 46, this should be entry. Actually, what am I talking about? This should be self.orinsertwith. Can't compare line 87 with the key. All right, yeah, so here's the compile error that I thought it was gonna get and we did indeed. It's telling us that bucket is borrowed, but we're trying to move bucket down here. And that is because this find returns an option that points into bucket. So in the some case, we do actually have a pointer into bucket. So if we here try to move bucket, that would not be okay. In the none case though, we no longer have, we don't have a pointer into bucket. It would be safe to reuse bucket, but the compiler does not realize the reason it doesn't realize is because it thinks of borrows in the scope of blocks. So here, the thing that's returned from find is considered borrowed for this entire match, not just for the entry that gets the reference. There are two ways to fix this. So if you're on nightly, then you can turn on the feature NLL for non-lessable lifetimes, and that just fixes this issue. The compiler realizes that in the none branch, you don't actually hold on to the pointer and therefore it's okay for you to use buckets again. Because we want this to work on stable, because why not, we instead do this. It's a little less nice. But here, the reason this works is this reference is now only borrowed for the contents of this iflet, which means that after the iflet bucket is no longer borrowed, and so therefore we're free to move out of it. Feel like that's false. I have been working too much in NLL land, so it might not, like this is why NLL is nice, because it lets you get rid of so many of these things. Why am I not allowed to do that? Okay, fine. Buckets, bucket, iterate, mute. This should definitely work. That's really strange. Why is it complaining about that? And of course, this goes away. Can't compare because this, I am building a hash map. Well, we are building a hash map, but cannot borrow self.buckets as mutable more than once at a time. But I'm not, though. That's super strange. I have no idea why that is not letting me compile, but in order to not waste time on it, we're gonna implement it the other way. I'm gonna do this instead. So we're just doing the imperative way instead, because that might be why it's confused, but it shouldn't care. Entry, if entry.zero is key, then return entry. How about now? What on earth? Why is it not letting me do that? Oh, that's so weird. That is so weird. Well, if it's not letting me do this, then we will turn on NLL, because I don't wanna fight with this right now. So this is what we had. Feature, NLL. Turn on all the goodness. Rust up override set nightly. Cargo test. Am I doing something weird inside the... Oh, that's so strange. Buckets, bucket, here itself, bucket, bucket. I don't know why it's not letting me do this. If let's on entry, and I'll see something like obviously wrong here, because to me, this seems totally right. Oh, right. And also, why is it complaining about this now? If let's on that, then return this. Ah, there. Let's see. Let's see. So it's telling me that I'm borrowing from self.buckets here, and that therefore I'm not allowed to borrow mutably from self.buckets here, which to me is very surprising, because that would return, and there wouldn't be any borrow of self at that point anymore. Be a borrow of key, but that should be fine. All right, so let's try this again this way. So entry in muteself.buckets bucket, if entry.zero is equal to key. I have no idea why this is not letting me do that. Because entry here should be safe. So entry is a pointer into self.buckets. If we match it, then we return it, and return should be safe. So the question is why is this borrow of buckets not allowed when the borrow of buckets up here should be done at that time? And it doesn't even help scoping this? That's very confusing. I think this is a bug in the compiler. Well, in the borrow checker specifically. Because unless I'm mistaken, this should be perfectly fine. Weird. A little bit annoying, because I don't have a good way of getting around this. The bucket, so it's borrowed. First mutable borrow occurs here. Borrowed value must be valid for the enormous lifetime to find there. So let's name the lifetimes and see if that helps us. Borrowed value must be valid for the lifetime A. Oh, I wonder. I wonder if it, oh, it's because the entry, the lifetime for this borrow needs to be the same as the lifetime for this borrow because returning an entry take A. I wonder what they do in the standard library for this. I guess we're about to find out. Well, that's interesting. Source, gimme that source. What does this into entry do? Well, that's pretty unhelpful. Well, I mean, we can technically get around this with unsafe, which I think is okay. But it's probably gonna come back to bite me. But sure, let's do some unsafe. Sounds good. Specifically, the reason we can do unsafe here is because we know that either we will return this or we will return this. And both of them are tied to the lifetime of self. So this shouldn't actually be unsafe. So this will be mute star entry. Specifically, what we're doing here is we're dereferencing and then re-referencing the entry, which I think should erase the lifetime. So we want entry as mute and then we will... So we're basically converting it to a pointer and then back out. So the trick we're pulling here, yeah, I know it's pretty sad. It shouldn't need to be unsafe. I think it's that the compiler doesn't realize that these two lifetimes are non-overlapping. I'll probably file a bug afterwards because this should not be right. Actually, let's go back for a sec and do just like checkpoint this. Almost, but needs unsafe. Bug, question mark. That way I can file it later and hopefully it will be, hopefully the smarter Rust folks than me will figure out what's wrong. And then we'll go back to this. Oops. And hopefully eventually we can get rid of that. Okay, so now I think we can run the example. Ooh. Attempted to calculate the remainder with a divisor, divisor of zero. Ah, okay, so imagine that the very first thing you insert into the hash map is using entry. So remember how in insert we have this like trick where if you do an insert, we do a resize if necessary. I'm gonna have to do the same thing here. Specifically, if you decide to insert into a vacant, into a vacant entry, then now we need to, first of all, we need to update the number of items in the map. And second of all, we need to resize the map if it's too small. So I think what we're gonna do here is we're gonna have this actually contain a pointer to the map itself so that we can update things appropriately and then we'll have it store the bucket as in the index of the bucket. Because that way when we do or insert up here in vacant, this can now do the same kind of logic as our insert did. Specifically, this is now gonna be a map which is gonna be a hash map KV and a bucket which is gonna be a U size. And now when you insert using a vacant entry, if we detect that the map has no buckets or if the number of items in the map is more than three quarters, here technically we should probably have a helper method for this. But basically if we detect that those things are the case then we resize the map. And then we do self map buckets self.bucket push self.key. We do self map items plus equals one because there's now one more item in the map. And then we return a reference to the thing that we just inserted. This of course has to call resize on map so you can't resize the vacant entry. That does mean that this now needs to require that K is hash and Eek, although in general you can't, ooh, you in fact can't even, so for the entry API, you can't even get an entry without K being hash and Eek. So that should be fine. Now, ooh, that's a lot of complaints. Oh, did I, I don't understand why I keep putting colons afterwares. Eek is not implemented. And finally no field one, why? Oh buckets, right. We need to find the specific bucket that we inserted into and then we find the entry that we just pushed and then we return a pointer to the bottom. And now it's telling us that we cannot borrow immutable items self.map. That is because self here is not mutable. Sorry, that's because this has to be a mutable reference to the map, because otherwise we can't change it in any way, right? Now what is the complaint about? It's complaining that we can't, trying to divide by zero again on line 94. So that means bucket got called from somewhere when we hadn't even resized. That's because we compute the bucket here. Yeah, so we actually do need to do the resize outside. Okay, that's fine. It's a little sad because it means that if you use entry, get a vacant entry and don't insert anything, then you still now cause the map to allocate, but that's probably fine. So we basically do the, we do the resize in entry so that we guarantee that in fact we might have to do it here anyway, because imagine that the insert caused the resize that would change which bucket you end up putting stuff into. And now this is just gonna be self because there's no self.map anymore because we're already on the map. Hey, didn't print anything though. That's a little bit sad, but this does mean that the entire example succeeds. So one thing we could do, now we don't need NLL anymore, which also means that we can move the override. Great. So let's now try, what's the last thing we have for Hashmap here? Oh, this is just some other example that they have. This other example I think is mostly just to see that you can use other types. Standard one. So if you see this example, it is mostly just testing that the map works with some type that implements eek and hash, but that is not like string or use size or anything. So this should just work for us out of the box. Great. This thing is a little neat. So, let's see, examples, standard forest. Okay, so this example is constructing a map by taking a list and collecting it into a map. So remember that in Rust in general, you can have a list of, you can have an iterator and on iterator, where is the iterator? The iterator. So the collect method on an iterator takes all the things that you've been iterating over and collects them usually into a vector, but you can also, if your iterator is over tuples of two elements you can also collect into a hash map, which is really, really useful. And we sort of want our hash map to be able to construct it that way too. And you see that in order to get the collect method, you need B, so the thing you're collecting into to implement from iterator over the things that the iterator yields. So in our case, that would mean that we would have to implement from iterator for a map. So let's see, we're gonna add examples two and three entry API. And now for example, four, what we want to do is we want to implement from iterator for a map. Imple K and V, from iterator for hash map. Looks like from iterator is not in the prelude, so we need to import this. Let's see, so for from iterator, so it's a generic trait A, so specifically we want to implement from iterator over things that are tuples of key value. So if you have an iterator over key value, you can collect into a hash map with that key and that value type. We are the from iter method is generic over T where T is the iterator. We could call this I if we wanted to, I prefer I for iterator, it's not terribly important. So we can take any, we can take any iterator, anything that can be turned into an iterator whose items are key and value. And so what we would do, there are a couple of different ways we can do this. We, the easiest way is to say mute map is hash map new and then for K, V in iter, map.insert KV and then return the map. They mess up the syntax here somewhere, probably, oh use. And of course this requires also that where K implements a hash unique. Great. So the idea here is that this is a very straightforward implementation, right? We have a map and we iterate over the iterator we're given and for every key value pair, we insert that value with a given key and then we return the resulting map. Now you can be a little bit more efficient here. So for example, if you know, so the iterators for example, have this size hint, size hint. So size in tells you how many elements may be coming down the line. And if you know how many items may be coming then you can essentially pre-allocate enough space to fit all of them. So remember now we just like double the number of buckets whenever we detect that it's starting to get full. Instead of doing that, we could say that whenever, in advance I can allocate space that will hold about this many items. And that way I might not have to allocate at all in the map while I'm iterating over this. In the current state of things, I'm gonna keep doubling the number of buckets, some number of times, basically log n of the number of items in the iterator. And that's probably fine in our case. We don't, this is not a very performant hash map anyway. But I think in the standard library for example, when they implement from iterator, they make use of all of these size hints for iterators for example. Similarly, while we're at it, the other thing we'll want to implement is, remember how we have this into iterator for hash map? So specifically for a ref, so you can get a reference to all of the items in the map. We might also want a way for the user to just extract all of the things in the map. Like they want to think of this as drain. Actually, we can implement drain too. We want something that just like gives you all of the items. And we can do that pretty efficiently actually. So we might as well do that because it's a feature that people use. Let's commit this first though. So this worked. So we will add the example city four from iterator. So now you can collect into our new map. And finally, let's add into iterator for a map itself. So this would be, if you wanted to extract all the key value pairs, not by reference, you just wanted to like drain, you wanted to take everything from the map. So this is not quite the same as a drain because a drain leaves the map intact. It just removes all the key value pairs whereas this will also deconstruct the map. So this is gonna, it's going to basically be the same. It's gonna be a little bit different because it will just contain a, it's a couple of ways we can do this, but we can actually have it be basically the same as what we had up here. So we have an iter and then we will have this, which is gonna be an into iter, which has pretty much all of the same things, but it owns the map, it implements iterator, but it implements iterator over K and V directly rather than over references to them. And apart from that, instead of doing, oh, yeah, let's do getMute and this will do, so that we sort of want this to do a remove, right? But remember how I mentioned that if you remove something from a vector, then you have to shift all the things to follow it. So we sort of want to use swapRemove here, but unfortunately swapRemove returns a T and panics if it's out of bounds. So remember how we rely on the fact that we can detect if self.add is moved past the end of a bucket and move to the next bucket. SwapRemove would not give us that information. So we need to basically reverse this flow a little and say that if self.add is more than equal to bucket.length, then we've spilled over. Else we break with some of self.bucket, sorry, no, bucket.swapRemoveAt. Now you might also notice that if you're very perceptive, swapRemove remember moves things around. So this means that we will not actually iterate in bucket order because when we get element zero of some given bucket and yielded for iteration, we're going to swap zero with the last thing in that bucket. The next iterator, ooh, actually this is wrong. At here is always zero because we use swapRemove. Aha, that's actually kind of nice. We're going to keep removing the zero element. So we're going to remove the zero element and then the last element gets placed in the zero spot and then it gets truncated. So there will always, if there are elements left in the bucket, they will always be at least one in index zero. So if the bucket is empty, we move to the next bucket. Otherwise we just take the first element. And so we don't actually need at at all when we're draining like this. The reason we can't do this in the other iterator is in the other iterator, we're not removing anything. So you can't like remove element zero. You could technically swap things around but there's no reason to do that. You can just walk the bucket instead. Whereas here we just want to keep removing things from the bucket until each bucket is empty. And we could test this by saying that by essentially doing the same thing as we did here, actually extract the K and V from the map. So this is the old test we had for iterators. And we're saying here items is zero, items plus equals one and then we assert that we indeed got four items. Is it mildly more efficient to remove the last item, not swap and remove the first one? Do you mean then remove the first one? So we could, yeah, so the proposal here is we could instead of doing this, we could do bucket.pop. That works too. I think they're basically about the same. But yeah, so one way we could keep this code sort of similar to what it used to be is we do pop, which does what was suggested here and removes the last element. And if it's sum, x, then we break with x, with sum x. Otherwise, so if you pop and there's nothing there, that means that it was empty and so we continue. Yeah, that's pretty nice. I like that. So this still ends up doing exactly the same thing, except that it removes from the end rather than removing and swapping from the beginning. I think you would struggle to find a performance difference between these, but this is probably like probably better, I would say. Let's see if this works. Now also let's fix this so that it doesn't give us a warning. So now we also have into iterator. And now that we have into iterator, we could also implement drain pretty efficiently. And drain, let's not do it, it's fine. So drain is sort of a similar kind of, we put an iterator struct there, but what it's gonna do is it basically does exactly the same as our into iterator does, but it does not consume the map. It just empties all the buckets. So it would be just like this one, except that this would be a mute hash map instead. Apart from that, the code is exactly the same. It just keeps removing all the things from all the buckets and then returns the, and then at the end, you have a map, but it's a, oh, actually, we can do this with a cal. Unfortunately, you need a different implementation because one of them needs to take this, whereas the other one needs to actually own the hash map so that the hash map gets dropped at the end. And so you can't actually have them be quite the same. There's the cal type, which is really nice, which lets you capture either owning or having a reference to, but unfortunately it doesn't capture having a mutable reference or owning. That's not something cal deals with. Okay, I think we're like pretty decent with this now. We support, there's a lot of things in the hash map API that we don't support, we support all of the examples that they give, all of the basic features. The things like with hasher, like we talked about in the very beginning, there's with capacity for learning about how many things you could fit in the map before it would need to be resized. Now, capacity works a little bit differently for us because our map is linked. There is no real capacity. You could always insert more elements. They would just get, your buckets would just get really long and you would have to search them. Whereas in the Rust standard library hash map, because it's using open chaining, so there's no, every bucket holds exactly one, or at most one element, and other elements that are pushed, just get pushed to other locations in the map. Because of that there's actually a capacity. The number of buckets you have is the number of elements you can insert. That's not the case for us. So that's why they need more of an API around capacity and reserving additional capacity. We could still approximate that because we know that we resize whenever there are more than three quarters of the number of elements. There are also other strategies we could use, if any bucket has more than five elements, then resize, which would be a different kind of capacity reservation. And notice also they have operations like shrink to fit. So this is reducing capacity after you've resized to grow. So that's something we could do too. If we detect that after we remove, there are fewer than this and this many elements, then make the map smaller. There are other iterators, like things that give you only the keys, things that give you only the values. I don't think those are terribly important to check. This clear, which is actually pretty useful, is a clear empties all the buckets and drops all the values. But it's also not terribly important. Yeah, here you see the same trick we pulled us with borrow. And they have that for remove as well. Remove entry, I don't know if remove entry does. Oh, so remove just returns the value, remove entry returns the key and the value. Does that make sense? Retain is just like it removes anything. It just does an iteration and you call the user provided closure for each element. And if the closure returns true, then you keep the element, otherwise you remove it. So the reason, actually I should mention this. The reason that occupied entry just holding entry is not really sufficient is because normally there's an implementation on, there's a function on occupied entry called remove where you remove an entry that was already there. And in that case, it would actually need to keep a mutable reference to the map. So we could say, decrement the number of items in the map. And also, if it just has a mutable reference to the key value, that means it doesn't get to change the vector. Whereas if you remove something, you actually need to do a remove on the vector itself. Like from the bucket, you need to remove this key value pair, which you can't do with just this pointer. So that's also something we could add. When you match to a monad like sum, is there a way to bind the entire monad instead of having to wrap it? Yeah, so you can. Let's see if I have a good place where I do that kind of match. I guess we did that in an iterator, right? So here, yeah, so there are a couple of ways you could do this. You can just do X. So you can just match the name. You just don't destruct it, right? The other thing you could do is if you want to do it, but so if you did this, then none would never be hit. So that's not okay. What you can do is you can do this. So this will match against the pattern, but bind X to the whole thing. This would mean that you're not allowed to do this because you can't both move out of the sum and keep a reference to the outer thing because the inner thing has been destructed. But you could do this. Yeah, exactly. Yeah, so these kind of add matches are pretty useful when you have very large matches where you are sort of, usually if you have something like a single channel coming in and you want to multiplex the multiple channels, you often want to match without stealing things and then just forward and now you can do that here. All right, I think that's like about all I wanted to cover in this first one. The one thing we could do, what's that time? Yeah, so one thing we could do, we do have sort of the time for it is to, we could have the buckets be sorted. So currently the buckets are in random order. This is why we have to walk the entire bucket to find a element with a given key. Whereas if they were in sorted order, then now you can just do a binary search on them to find the appropriate key. And that is a lot more efficient for lookups, but it is much slower for inserts because insertions now need to do basically an insertion sort. Like they have to find where to insert, put it there and shift all the other elements over so it makes inserts a lot more expensive. If people are interested in seeing like a sorted bucket version, we're basically just gonna take this and ensure that we maintain that it's sorted in all places. It's unclear whether that'll be interesting, but if you feel like it'll be interesting then give a shout out and I'll consider it. The other thing I want to do later is maybe try a more intelligent hashing scheme. So for example, you could do cuckoo hashing where cuckoo hashing is an open chaining scheme where you use two hashing functions. So you don't chain any of the buckets. So then we would have capacity in such like the standard implementation. And you get both the hashes for a given key. You check if either of them is empty and if they are, you put it in the one that's empty. If neither of them are available, you pick one and then you check its two alternatives and if one of those is empty, you move it to the other one and now you have an empty space for the original one and you keep recursing like this until you find a place where you can insert. It's a pretty neat data structure. Pascal, one of the, another Rust developer pointed out that we could also do hopscotch tables that are pretty neat. Or of course, I also want to cover other things in the standard library. Like it would be fun to try to implement a mutex which has a bunch more unsafe code. The hash map shouldn't have any unsafe code but unfortunately happens to have one that hopefully I'll get rid of. I'll follow the ticket after this. Yeah, I mean, in general, what I do for these streams is I post a Twitter about a week in advance with a poll of what you would like to see. So that's how we ended up doing a hash map now. There were a fair number of votes for continuing on on the asynchronous SSH crate that we wrote a few streams ago which is currently only supports reads but to have it support writing to the standard input of a remote process as well which I think could be pretty cool. It would be probably a little bit more advanced than this but if people are interested, I'm happy to do those too. All right, it doesn't look like people are terribly excited about doing sorted buckets which I sort of understand. So I think we'll probably call it here then. Thanks for coming out, thanks for watching. If you have ideas for other things you'd like me to cover then ping me on Twitter or on Patreon. If you feel like there are things that could be better in the streams like someone pointed out last time that the font size was too small and so that's why the font size is now larger. Then just like, let me know if there are particular things you'd like to learn if there are particular things you think should be explained better, then let me know. Apart from that, enjoy the rest of your day. Thanks for coming out. Bye everyone. Let's see if I...