 it's time for another crust of Rust stream. And this time we're gonna be looking at iterators because iterators are something you're gonna run into a lot in Rust. You may or may not know where iterators actually appear. You may have used them without even realizing. They have some relatively interesting trade bounds once you get to more advanced uses of iterators. And so that's what we're gonna try to dig into now to basically use iterators to teach us about trade bounds. First though, as sort of a starting point like we do for all these streams, I'm John. I do a bunch of these sort of live coding videos and educational videos, especially around the Rust language. You can find announcements of upcoming streams on my Twitter account. You can also subscribe on YouTube or on Twitch and you'll get notifications whenever I post a recording of a video or whenever I go live. I also have like a wish list there if you have some way to like support the show. But apart from that, how about we get started on iterators. Same as with past streams, like past cross-over Rust streams, I will be sort of monitoring chat as we go along. So if you have questions, please ask them. The people who are currently in chat, like keep in mind that if you have a question, chances are some other person who's watching the recording afterwards will have the same question but they don't have the opportunity to ask. So like ask your questions. I'll try to pick the ones that seem pretty common and seem like things that need to be explained in order to go on. I won't go on too many tangents beyond that. All right, sweet. The text on this particular page is not important. When the text is important, I'll zoom in. Great. Okay, so we're gonna talk about iterators. And in order to talk about iterators, we first need to understand what an iterator is. An iterator in Rust is a trait. It's a trait that has, actually, let me, here I can zoom in. So iterator is a trait. It has two primary things that you need to be concerned with. It has an associated type called item and it has a method called next. And perhaps intuitively, the associated item, the associated type item is the items that will be yielded by the iterator. And next is gonna be called by the thing that drives the iterator and is gonna yield some of the item type until the iterator has been exhausted, at which point it yields none, and the iterator terminates. So let's look at some, let's just sort of start a new thing here, iterators. And look at some code, cause it might be instructive. When you write something like 4x in vec A, B, C, right? Imagine you write some code like this. What actually happens under the hood is a little interesting. There aren't really four loops in Rust once you get below a certain sort of level of sugar or syntax, if you will. Really this turns into, well first, it turns into iter, into iter. We'll talk about what into iter means. And then while let some, whatever, E is iter.next, right? So notice that this is basically the same thing. This is a de-sugaring of what we wrote before. So the thing that was passed as an argument to four, we call into iter on. This gives us an iterator over whatever that thing is, assuming that it is something that can be iterated over. And then the for loop can go away and be replaced with a while let loop that just keeps calling next while it's returning some, which means that there's more stuff in the iterator for us to look at. And the moment it returns none, it stops iterating over that thing. The syntax sure isn't exactly like this. In reality, I think it actually just evolves into a loop with a break, but this gives you enough of a flavor for what's going on behind the scenes. So before we start looking at the particular iterator implementation we're gonna be looking at today, let's take a look at this into iter just briefly. So into iter is a separate trait. So if we go to the iter module down here somewhere, you will see that there's a trait called into iterator. And into iterator is sort of a wrapper trait around iterator. It is really just anything that can be turned into an iterator. And anything that is an iterator can obviously be turned into an iterator, but other things can be turned into iterators. So for example, if we look at some of the implementations, you'll see that there are implementations here for things like hash maps, hash sets, there are implementations for slices, references to slices, mutable references to slices, mutable references to hash maps, all of these things, if you have one of them, you can get an iterator over some relevant part of them. So for example, if you have a mutable reference to a hash map, right, then into iterator is implemented for this type for that mutable reference. And the item that you get back, the things you're gonna get back from the iterator is a tuple of an immutable reference to a key, so the shared reference to the key, and an exclusive or mutable reference to the value. This makes sense, right? If you have a hash map, it has keys and values, you can't change the keys as you iterate over them, but you can change the values. You can change the keys because things would then move around under you in the map, which is not okay. Intuitor also has this associated type of intuitor, which is the type of the iterator that it's going to return. And so in this case, iterMute is a type that's implemented internally in the hash map submodule. We don't get to look at it here because it's not really that important. And we're not gonna look too much at intuitorator here, it's just a useful thing to look at. Okay, so back to iterator. One thing you might wonder is why is this an associated type? Why do we have an associated type item as opposed to having item or i or t be a type attribute on the trait itself, right? So what I'm talking about here is why do we have iterator item, it will type item and mute self returns an option of self item. Why is this the trait? And not this, right? Like why the form or not the latter? And the reason why we use the former and not the latter is because if we use the, so there's no hard and fast rule for this, there's no right way or wrong way, both of these would work. In general, you use an associated type if you expect that there will only be one implementation of the trait for a given type. So if you have a type like hash map, there is only one iterator type for it, which is keys and values. If you have a vector, there's only one iterator type, which is something that yields the values of the vector, right? And in general, this is gonna be true for iterators that they only have one item type that is meaningful. On the other hand, you want things to be generic type parameters to the trait if multiple implementations might make sense for any given type. So let's look at some hypothetical service trait that has a request type. And like who knows what this does, right? This do, and it takes a request. Mute, self, and R requests, and it returns nothing, right? This trait, we hear we could also have requests be an associated type, but it might make sense that you could have some service that supports accepting multiple different types of requests. If we had requests be an associated type here, then that would no longer work. You would only be able to implement this trait once and give what the associated type is. This might make you wonder, isn't the generic type parameter just more general? Like why don't we use that always so we never constrain the implementer? The answer to that is a little complicated, but the general idea is that if you have it be an associated type, the type checker has a much easier job of things because now it knows that there is only one implementation to choose. And so there are fewer times where you need to write implementation blocks, for example, or function signatures that have additional generic parameters. The associated type is sort of dictated by which underlying type it is. So if I give you a something iterable, and you say that you want to use this as an iterator, I immediately know which iterator implementation you mean because there is only one. If on the other hand I give you a something service and you want to use it as a service, you have to tell the compiler which request type service do you want it to be. So in general, associated type reduce the amount of extra generic type parameters you need to be using. Okay, so that was a sort of long aside on traits and associated types. Let's look through some questions before we move on to actually implementing an iterator ourselves. Why do some types of iter and others use into iter? I don't think there's a good reason to have an iter method. Like if you have a slice, you may have seen that if I have like an A comma B, well, it's a bad example, but I can call dot iter and it gives me an iterator over the elements of the slice. So you might wonder, well, why doesn't slice just implement into iterator? And the answer is it does. Usually these methods are unnecessary, but they might, they're shorter to type if they happen to come up very often. And they also let you, they mean that you don't have to conform exactly to the method signature that the trait, the into iter trait requires, which in some cases make lifetime reasoning easier. Couldn't HashMap also have separate iterators for just keys or just values? It could and it does, right? Like it has a HashMap, for example, has inherent methods. So those are methods implemented directly on the type rather than through a trait for the one called dot keys and one called dot values. But HashMap itself does not implement iterator. It implements into iterator. And if you have a HashMap and you say, give me an iterator over it, there's only really one answer because it doesn't take any other arguments, right? If we're using a for loop, we don't need to explicitly call dot iter. That is correct. Although keep in mind that if I have v's, is a vec of one, two, three, there's a big difference between this and this. This one consumes vs and gives you owned access to v. This one borrows vs and gives you references to the v's. Now, if you don't want to write the iter, what you can also do is this. This is equivalent to vs.editor. These are the same. And this is because into iter is implemented for reference to slice vector. Why does range inclusive? That seems separate. Great. Increase font size. Yes, I can. There we go, that's real big. Yeah, so someone pointed out that it's strange that the consuming iterator is the implicit default. I don't think that's strange. You said iterate over this thing and that's what it's doing. It's iterating over the vector. If you want to iterate over a borrow of the vector, you have to explicitly say that you want to iterate over the borrow of the vector. This is sort of a general idea and rust of being explicit is good. And here's an example of that. It's not gonna auto borrow unless you tell it to. Great. All right. So now let's get to what I was planning to do today, which is you may have noticed that there are a bunch of other methods on iterator and these you can tell from the little bit at the end here have default implementations. That is the standard library or the thing that defines this trait, which in this case is the standard library has default implementations on these that just use this type and this method. So for example, the default implementation of count just calls next until it returns none and keeps a track of how many times it got a sum and that is the value. Of course, for some iterators, you can imagine a much more efficient implementation of count, right? So the implementation of iterator count for a vector can just like the vector knows its length so we can just return that rather than iterate over all the things. And this is why these have default implementations because they're saying defaults for them, but you want them to be able to be overridden for particular implementations of iterator. And there are a lot of really cool things here, but the one I particularly want to look at today is flatten. So what flatten does, which we can see from the method signature two is it takes an iterator, right? So this is a method on the iterator trait so by default self must be something that is an iterator. And it's only defined where self item implements into iterator, right? And then what flatten actually does is it walks the elements of the outer iterator and for each item, each item is expected to itself be able to be turned into an iterator and then it walks all the inner things before returning to the next outer thing. A diagram might be useful here so let's actually draw a little. Let's do something like this color. So conceptually imagine our iterator some like endless thing of items, right? So this is gonna be our outer. And then we're gonna have an inner, which is when the outer iterator is about to yield its first item. Instead of yielding this item, what it's going to do is it's going to call into iter on that item, which again gives us another iterator. We don't necessarily know how long it is, it's just some other iterator. And then the first call to next is gonna return this item. The second call to next is gonna return this item all the way until this iterator ends. So these are all gonna be returned and then when the inner iterator has been exhausted what we're gonna do is at that point we're gonna move, oops, go back there. We're gonna then only then move on to the next outer item. Then we're gonna call into iterator on that and then we're gonna iterate through the things inside here and then so on. So it flattens a nested iterator type, right? So imagine for example that you have a vector of vectors. Flatten would iterate over all of the items of the inner vectors in order. Does that roughly make sense? Just let's make sure that we're on the same page. Okay, so that seems to roughly make sense, great. So flatten is a fairly straightforward in terms of how you use it. But let's try to actually implement flatten. So let's have a library. It has a pubfn flatten and it takes an iterator. Does it flatten infinitely? Well, it keeps taking as long as there are iterator items left, right? The moment it has consumed the entire outer iterator then there's no more items to yield. But it keeps taking until the none of the iterators and the type you're iterating over yield any more items. Do these nested iterators need to have the same item type? Yes. So, I mean, this is already a requirement of iterator. That all of, think of it this way. If all of the blue things have the same type then calling into iter on each of them must also by necessity give the same type. And so we don't actually have to explicitly say this. It's just, this is just implied. It does not, it only recurses one level. So this is not recursive all the way down. It's not turtles all the way down, right? It only recurses one level. So it's not recursive. Okay, great. So let's look at what the interface to this look like. We're gonna start with having this just be a freestanding function and then we might see if we can do better than that after a while. So, we take an iterator and it's gonna be of some generic type I. I haven't decided what I is gonna be yet. And it's gonna return some type flatten that obviously need to include that I. And this is probably just gonna be like flatten new iter. Great. So we don't have a flatten, that's fine. We're gonna define a flatten. It takes an I. And let's start with something that just holds the outer iterator. Let's call it outer, right? We're gonna do, Imple I flatten I FN new iter I. And this is standard like set up stuff, right? Great. And this has to be generic over the iterator type. Okay, seems fine enough. We now have a type and now we wanna implement iterator for flatten. And we need to define then the item and we need to implement next. Those are sort of the two primary requirements for us to do in order to implement iterator. So what do we set item to here? Well, we don't actually know what the item is here, right? We could do I item, but that's not really the type we're gonna be yielding because we're gonna be yielding the type of the inner iterator where I here's the outer iterator. So let's go ahead and actually do that straight away. We're gonna make this O to indicate that it's the outer iterator just for our own sanity. And then this makes a lot more sense, right? Like it's not the outer item we're gonna yield. So clearly something more is needed here. Well, we know that we're gonna need to have at least some bound that says where the outer thing implements iterator. If the outer thing isn't iterable, then we're not gonna get anywhere. I guess let's make this none. Okay, so that compiles, but again, as we said, this is not right. What we really wanna say here is like this, right? Like we want the item of the item, but Rust is like, I don't know what you mean here. Like what is this associated type? Well, we can do a little bit better. So what we wanna do is we wanna define additional trait bounds here. We want to say that O item also needs to implement into iterator, right? And then we wanna say that the return type here is gonna be the O item as into iterator item. This is syntax makes sense, right? So here we're saying the outer thing is an iterator and the items of that iterator. So this could also have been written as O as iterator, but Rust is helpful enough that it lets us omit this when there's only one trait bound of O that gives an associated time for this name that we don't need to give this name explicitly. So we want the outer thing to be an iterator and we want the items of that outer type to implement into iterator so that we can then later iterate over them. All right, we have of course not written next yet, but does this part make sense? Why is O item as into iterator? Yeah, so this is sort of the same question, right? If we could omit O as iterator here, why can't we do the same here? It's a good question. Let's see what cargo check says. Let's do beta here. I don't know why he thinks that's ambiguous. I think this is a parsing ambiguity. It doesn't know whether this is unclear. It could be a parsing ambiguity. I think in general, like this could be a path, for example. Mike, you're confused about that. I'm not sure. It's certain that as long as it's parsed as a sort of nested associated types, then it's not ambiguous. So I don't think this is technically needed, but it seems fine. Could you add another generic type with bounds? Yeah, so you could flatten this a third level if you wanted to, right? You could say, and then I want O item as into iterator item to implement into iterator if you wanted like a three level deep flatten. But notice that this would only work for things that are in fact three levels deep. This would not work for things that are two levels deep because this last bound would no longer be true. But let's stick to just two levels for now. Oh yeah, we might be able to do this to see. No, apparently it doesn't like that. That's fine. Great. So now let's try to actually write next. Okay, so we could do self.outer.next, right? That seems like the obvious thing, but then it tells us this is wrong, right? Because the type of this is O item, the item of O. Whereas what we promised was that we would give the item of the iterator of the item of O, right? And maybe this is going to be clear in a terminal, right? It expected to get like this sort of nested type and instead it just found O item, O is iterator item because we haven't nested at all. So what do we do? Well, we could do like map and like inner, inner dot, I guess this would have to be an and then inner next. Well, that doesn't work because it's an intuiterator, not an iterator, so it doesn't have a next. So intuiter, great. Okay, so this compiles and obviously there's nothing wrong with this code. You could write a macro that generates an implementation for an arbitrary level, probably as a proc macro. Oh yeah, you can flatten recursively yourself. As in like you can invoke flatten multiple times. Flat map is a good way to do this actually. Okay, so let's, now that we're totally done, let's do tests. View super, then we're gonna have a test. Well, let's do first empty. So what's this gonna do? Well, this is going to do a flatten of inner empty and we wanna assert that that count is zero. And I guess we're gonna have to say what type we're iterating over because it can't infer it because we haven't given it any values. And the, so this is actually kind of interesting. So empty returns you an iterator that yields no items but because there are no items the compiler needs us to tell it what the iterator item type is. I tried to use this unit here, like the empty tuple but then it says, oh, that won't work because flatten, it's easier here. Ooh, tests. The method count exists but the following trait bounds were not satisfied. Unit does not implement into iterator which is required by flatten in order for it to implement iterator, right? So it's actually telling us pretty nicely here exactly what's going on. You can't use flatten as an iterator unless the item type which here is unit implements into iterator. Since this is empty though we can just tell it that it's some other type that is totally iterable like this. Okay, let's see if that works. Great, an empty iterator works correctly. All right, now let's see if it works if we have just one item. So this is gonna be, it yields only one item and that one item is gonna be let's say a vector of zero. So that failed and it failed because it got a one when it expected a zero because there is one item here, right? If you go all the way down, you will get this zero. Let's make it not zero, let's make it A, right? A should be counted so the count here should be one. Let's see if that is in fact the case. That is in fact the case. All right, so now let's go even weirder. Let's try to make this, let's try to iterate over more items. Let's do B as well and see if the count is two. What? The count is only one. Okay, so this clearly implies that our iterator sort of works but it only works for the first item which is clearly pretty useless. So let's, maybe we sort of got it right. Like let's see what happens if we try something else. Let's see if we try a vector of vectors where they're in different vectors. And I guess here actually we're gonna have to do, we're gonna do into iter here for relatively uninteresting reasons. Let's see what this does. Ooh, did I miss a parenthesis? And terminate these with semicolons. And so this is sort of a two wide. So here there is the outer iterator has one element and the inner iterator, the first inner iterator has two elements. Here the outer iterator has two elements and both of the inner iterators have one element each. Let's see. Okay, so that worked. Right? So clearly it is walking over the outer iterator but it's only returning the first thing from each inner iterator, right? And we can test this with something that's wide as well, right? So another way for us to get empty is to have this be just a ton of empty things. Right? If you flatten this iterator, I guess, fine. We need to actually tell it what type it is. Like so, like so. So this, when you flatten this type, that still has no elements in it, right? The outer iterator has three items but each of those items iterators have zero items. So if you count them, there are zero items. Let's see that that, in fact, works out too. It does. Okay, so the only problem here seems to be if any of the iterators have more than one item, right? It works fine if they're empty. It works fine if they have one item. It does not work fine if one outer iterator element has multiple inner items. So let's look at our implementation why that might be. Actually, let's see if there are questions first. Yeah, so we could also do standard iterator empty whose type is an empty iterator. But we want the test to be easy to read where we can. Okay, so let's try to see what's actually going on here. Maybe this is gonna be clear if we use question mark. So question mark also works for options. So we can do this. All right, actually, let's, I guess, inner. This is equivalent to the code we wrote before. Inner it. Well, I guess inner item. Inner it. And then we want inner it next. This should be item. And I guess if we're gonna be really explicit about it, we do this. Now that the code is written out this way, it might become a little bit more obvious what the problem is. So we call next on the outer iterator. And that is the first thing we do anytime someone calls next. This means that every time next is called, we're gonna be moving the outer iterator along. And when we get the inner iterator, right? So here we take the item that was yielded by the outer iterator, the sort of green long iterator. And we call next on it, but then we just drop it and we never get to touch that again. But in reality, this inner iterator might have more items to yield beyond just the very first one. So what do we do with this? Like where do we stick to this inner iterator? Cause we're going to have to stick it somewhere so that we can continue to call it. Well, we have a struct. So how about we do inner? And what type do we give this? Well, O item, but that means we here need to say where O is iterator. And that means we here need to say where O is iterator. And that means we here need to say where I is iterator. Now, this particular problem of like the bounds propagating all the way up is something that will get fixed a little bit by something called implied bounds, where if you have a bound that's on the struct, you don't need to name it again for any implementation. That is not currently the case. But that will help. And so when someone creates a new iterator, what do we do? Like what do we set inner to here? We don't have an inner iterator. So what we actually do here is this probably just needs to be an option cause we may not have one yet, right? And so now let's look at what we need to do for next. What we want to do is if there is an inner iterator that we're still working on, then we want to give items from that. It's only if the inner iterator has been exhausted that we want to move out to the next outer element. So if let some inner, let's do iter to make it really clear. So if there's an inner iterator and if the inner iterator, when we call next on it, gives us an item, then we want to return that item. If it doesn't, right, that means that we've exhausted the inner iterator and self.inner can be set to none. Why is it complaining here? Method next. Right. All right. So why is this a problem? Well, the item type, as we see, does not actually implement iterator. It implements into iterator. So we can't call next on it cause we can only call next on it when it has become an iterator. The example here would be the vector type, right? So the way this is currently written, inner would be an option vector, but you can't call next on a vector, right? The iterator implementation for a vector has some additional information like where in the vector am I currently? If we just here stored option vector, then we wouldn't have that information and we wouldn't be able to call next. So really what this needs to be is, oh item as into iterator, into iter, which is the iterator type of that item. And now we get through the same thing again, where we actually need to now require that the inner thing is also an iterator. You can see this gets a little bit unreadable cause we need to navigate our way down to exactly the type that we need to store. And then of course the trade bounds propagate up in the same way. And now, now we can say, I wanna call next on it. Okay, we haven't finished fixing next, but let's see if there are questions there. This is fairly involved what we went through just now. Yeah, the syntax here gets pretty gnarly, but it's because we're trying to name a type that's pretty deep, right? There's not that much extraneous syntax here, right? It's saying item as an iterator, or if we look at the bound up here, right? The contents of this is not actually that complicated. And the syntax isn't that complicated either. It's more that the bound needs to be there because it really is a restriction of our implementation. And so it needs to be written somewhere in some form. And I don't think this form is particularly bad. This form is a little bad. But again, there's not that much sort of extraneous information here. All right, so let's finish fixing our next. So if there's an inner iterator, then we're just done. Like there's nothing more for us to do. Well, we call next on it, and if it gives the thing, then we're done. However, if there's not an inner iterator, or if we exhausted the iterator just now, then what do we do? Well, then we just want to get the next outer iterator item, right? So we can do this, right? So this is give me the next item from the outer thing. Question mark means if there are no more items yielded by the outer iterator, then just return. Otherwise give me the item that was returned and then turn that into an iterator. And this is gonna be the next inner iterator. So now we need to store that somewhere, right? So we can do self.inner equals sum of next iterator. Great, but we then still need to actually get that item. So we could do self inner as mute, unwrap next, right? But that doesn't quite work either because this might exhaust the iterator itself. Like the inner iterator might be empty. So really the thing we're gonna do here is basically sort of do a loop where we're gonna end up recursing. And to see why this works, once we set the inner iterator here, we're gonna loop back around, it's gonna be set to sum and we're gonna do the right thing. And then ultimately either we're gonna return sum from some inner iterator or we're gonna return early with a none here when the outer iterator's been exhausted. All right, does this make sense for now? Do you normally include bounds on the struct definition as well or just the input block? I usually include them just on the input block when I can get away with it. Here's an example where we can't because the struct actually needs to talk about the nested associated types. All right, so let's see if this actually compiles and runs. It does. And now the tests actually work. Great, we successfully implemented flatten. And like there's a little bit of syntax pain here but it's not that bad. So let's look at some ways in which we can prove this. One obvious first fix is that we had to call into iterator, whenever we called flatten on something that isn't already an iterator. And that's kind of unnecessary. Like we'd like to be able to get rid of these into iterator calls and we can do that already with the into iterator trait. If we just do this, then, then now you can give flatten anything that can be turned into an iterator and it will call into it or for you and then flatten only has to think about iterator types. Would while let some inner equals self inner here? So the proposal is we do while let some inner is self inner instead. That doesn't quite work because think of the first time next is called. We need to make sure that we execute this code. And so that wouldn't work. Okay, so we fixed that sort of annoying, we fixed that sort of annoying just usability problem. But now let's look at some weirder parts of iterators. So if we go to the flatten from the standard library, you'll notice that it implements a bunch of other things. Here we can implement clone and debug, but those aren't that interesting for these purposes. But it also implements some other weird things. Most notably, it implements this double ended iterator trait. So what is double ended iterator? Well, as the name implies, it's an iterator that you can iterate from either end. So if we look at the declaration of it, it requires that the underlying type is iterator. And then in addition to the methods from iterator, so namely the associated type item and the method next, there's also a next back, where next back gives you the last element from the iterator. So you can sort of walk the iterator from either side. This means that you can really efficiently walk it backwards, for example. Great. So let's see if we can implement double ended iterator for flatten. In theory, it should be doable, right? Like if we think about this drawing again for a second, if the outer iterator lets us get to this item, then and that gives us some inner iterator, right? So if this is yielded, then that gives us some other iterator. And if that iterator also gives us the ability to look at its back, then it should be possible for us to implement next back too, just the same way as how we implemented next by taking the first item of the first, the first inner item of the first outer item, we can take the last inner item of the last outer item in order to implement next back. Should be totally fine, right? So let's see what that might look like. So here, what do we want to do? Well, we want to implement double ended iterator for flatten. Oh, and we're going to need to have the same trait bounce that iterator has, right? Cause these are just like fundamental to flatten. And then we're going to implement next back. And notice here that we can refer to self item here because, let me get rid of that, because double ended iterator sort of extends the iterator trait. That is a type can only implement double ended iterator if it implements iterator. And since iterator has an associated type item, anything that, so self, which implements double ended iterator must also have that same associated type. Here too, we could say self as iterator item, but we don't need to. Basically the compiler lets us omit that particular declaration if it's only one level deep. Great. So now how do we implement next back? Well, we already talked about this, right? Isn't it sort of just the same, right? Where it's the same thing it was did for iterator, except instead of calling next, we're going to call next back. Great. Well, this isn't quite work because next back, there's nothing that says that the inner iterators can be walked backwards. So we need to add those bounds too. So this needs to also implement double ended iterator. And O item as into iterator, it's into iter, needs to implement double ended iterator, right? So what we're saying here is the outer iterator needs to implement double ended iterator. Iterator is becoming a hard word to say. And in addition, the inner iterator, or the inner item when turned into an iterator, that iterator type, which is this associated type, needs to also implement double ended iterator so that we can walk the inner things backwards as well. Notice that the into iterator is sort of complicating things here, right? Because it's not as though the things that the outer thing produces are iterators themselves. We have to go through this extra step of turning them into iterators in order for us to be able to talk about them. And so this associated type into iter is the name of the, or it is the type, the iterator type for that item. So if the outer thing is an iterator whose items are vectors, then the into iter of vector is a vector iterator, which is its own type. And it's that type that we need to be double ended iterator. I know that this is like, there's a lot of levels that are hard to follow. I recommend that after we finish, like go back and look at the stream and try to actually write this out for yourself and see that this sequence of logic works out. We can luckily simplify this a little. So we can, for example, get rid of iterator there because double ended iterator implies iterator. But that's mostly what we can do. Yeah, so you can imagine that if we went three levels deep, this would get even more complicated, because the bounds need to apply at every level. What's the difference between plus and comma and trait bounds? Comma is just, here's a new rule. So this is one bound, this is one bound. Plus is saying both bounds must hold for the thing before the colon. So this is saying, O is iterator and double ended iterator. Whereas the comma here is saying, this must be true and this must be true. All right, so let's see if this works, right? So what can we do here? Well, let's try to reverse that and we're gonna try to reverse wide. So AB, if we collect that, should give a vector of BA. If we reverse that as well. And similarly here, if we reverse this and rev is a, in fact, we can look at this if you want, you'll see that on iterator, where's my iterator? There's a reverse that is the reverse method which only exists if the iterator itself implements double ended iterator. And since flatten now does, we can call rev. So here we're reversing and then collecting into a vector so that it's easy for us to compare it with things. And here too, this should give BA followed by A. All right, see what that gives us. Great, compiles the test run, we must be done, right? Let me give you a more complicated test. And some of you may already have figured out what's wrong here. It's more, it's useful to actually walk through it step by step, both ends. So here what we're gonna do is we're gonna try to flatten this thing. So what do we expect here? Well, we expect that if we take the first item then that should give us A, right? Cause if we just flatten this thing in our minds then the very first thing, so the leftmost thing is an A. We expect that the thing from the back, so now it's the remainder is like BCD. If we now get from the back, what we expect to see is D. If we now get from the front, we expect the next thing would be B and so on. And then once we've exhausted the iterator we expect all of these to be none, right? There after we've consumed A and D and B and C then now there's nothing left. And so trying to take from the iterator from either side should yield none. All right, so let's try that. Okay, that does not pass. Why did it not pass? So on line 126, so for this, for this next back, it expected, we expected to get a D and instead we got a B. So instead of getting a D here, we got a B. Why might that be? Let's see if we can shed some light on this by making this a little longer. See, let's do this like, this is gonna be A1, A2, A3. This is gonna be B1, B2, and B3. So now we expect this to give A1, this to give B3, this to give A2, this to give B2, this to give A3, this to give B1, and then none. And my prediction here is this is gonna give us A3. Let's see if that's true. Yeah, so this gave us A3 instead of B3. So why is that? Well, what's happening here is the next is gonna give us this item. And then for some reason, next back gives us this item, not this item. Hmm, how weird. Well, if you think about it, it's actually not that weird. Currently, we only have one field that stores the inner iterator. So we can only keep track of one inner iterator at a time. But in this example, when we call next back, what is the inner iterator? There's not just one, right? Because we're iterating over this thing because we called next. So we're sort of, we have a cursor that's like pointing here to A2. But when we call next back, what we want is a second cursor, right? That's gonna point to B2 after consuming B3. So what this is telling us is that we actually need to have two cursors here or two iterators that we may currently be in the process of going through. So this is sort of gonna be like a prefix iter, or if we will, a next iter, and then a back iter. And we're actually lucky here that this works out. We don't need to add double-ended iterator to flatten, right? Which would be inconvenient, it would be sad if you could only flatten things that were double-ended iterators. And the reason for that is because the inner iterator types are the same regardless of whether you go from the front to the back. The type is the same. Okay, so now next iter is gonna start out none, back iter is gonna start out none. And our logic here needs to be a little more clever. What we want is, if the next iter is some, then we wanna take the next thing from it, iter is none. Okay, what about this case? So here we have exhausted the, I guess this is maybe flipped from you. Now we've exhausted the iterator that we were at, and now we need to move to the next item. Well, if there's a next outer item, then great, we just give that next item. But what if, and we might need a drawing for this actually. So here's a weird case. What, ooh, why can't I do that? That's kind of sad. Doesn't my thing to move work anymore? My button won't let me move. Well, maybe I can just do this. So here's what's going on. I'm gonna draw this iterator in a slightly different way. And I realize I've flipped the colors. I'm sorry about that. So this is really what's going on. We have the outer iterator, which points to, I guess let's make some lines here too. The outer iterator is, let's say, currently pointing here. And the outer iterator, because it implements double-ended iterator, actually points both there and here, right? And when we call next back, we can then set our previous iterator to be this type. And we can set our next iterator to be this type. That's all well and good. But now imagine what happens if our back iterator is still pointing at this item, right? This item has not been yielded yet. And then we just keep calling next. So the next iterator finishes looking at this item, right? And then the outer iterator is called again. So that yields this item. And then we start looking at this one. And then we finish that. And then we call next again and we get this one. Now what happens if we call next again? Ideally, we should end up yielding this item, right? Cause it hasn't been yielded yet. But if we call outer next, outer next will give us nothing. Cause outer next has already yielded, like the outer iterator has already yielded all three items. It yielded this one, it yielded this one, and it yielded this one we initially called next back. So it's gonna return none. And the inner, the next inner iterator is also none. There's nothing more for us to look at in here. And so somehow this means that when calling next, when the outer iterator has been exhausted, you need to start walking the back iterator from the front. So that's what we're gonna do here, which is, I guess this is gonna be next inner. So if the outer thing does give us a next item, then we just set, then we do the same thing we did before. Right, we just set the sort of front iterator. Maybe we should call these front and back rather than next. Let's do front iter and back iter. So if the outer iter, if we're walking forward and the outer iterator gives us another element, then we just set that to be the front iterator and then we keep going, we just loop. But if the outer iterator stops iterating, it does not give us any more items. Then what we wanna do is start looking at the back iterator. And if the back iterator is also none, then we can return none. So question mark is fine here. And then we're gonna call next on that. So this way we're gonna, if we get to sort of the end, right, where we've exhausted the outer iterator and there's only one iterator left and that one inner iterator left and that happens to be at the back, then we're now just gonna be calling next on that. And then the same case is gonna be true for the back iterator. So here, let me just reuse the implementation we just used, except here, this is going to be looking at the back iterator and so the front iterator. And it's gonna call next back rather than next, right? It's gonna be next back inner because it's gonna look at the back of the outer iterator. And if we're walking backwards, backwards and the outer iterator yields no more elements, then we need to walk the front iterator from the back. And so that's what this is gonna be doing. And if the front iterator is empty, then of course we can just return. And what did I mess up here? This needs to be as mute. Cause we don't want to move out of it. We just wanna use it as a reference. All right, that was a lot. So let's do questions. Yeah, exactly. So someone observed correctly that the inner references were being reset in each direction, so it gets lost what the end should be. And so that's what we did. Is there a way to implement it in such a way that you don't need to have to pay the cost of two cursors when the iterator is only used in one way? It's a good question. There is, but I'm not gonna talk about it, I think. Basically the way, well, I'm not sure. I think you would need specialization for this trick to work, but the idea would be to add a second generic parameter to flatten that is either like, that is some type that implements like walk back. And you could give a zero size type to it that does not implement walk back in order if you wanted to use it without supporting double iteration. But you would need, I think, specialization to be able to implement a iterator for it. Because when you have, as we observed here, like when you have a back iterator, it changes the forward iterator as well. And so you would need to have a different implementation of iterator depending on whether or not you had two cursors. Yeah, so this gets back to the question that was raised. Why store back iterator even for types that aren't double-ended iterator? And the answer is we don't have a way around it. The flattened struct, we don't have a way to say here not if, like config not if, oh, implement, oh, does not implement double-ended iterator. Like that's not a construct that we have. Because it's not just the matter of defining two different flattened structs because then they would be different types. You don't really have a good way to say this. Keep in mind, though, that these will be allocated on the stack, so, I mean, they don't necessarily have to be, but these are likely to be cheap. Also, they're likely to be used as iterators and then dropped as opposed to being kept around for a long time. Great, let's see. This whole process gives me major future vibes. Yeah, I mean, it is a little bit like futures in that streams are iterators. And when you work with these nested iterators, it is very much like working with nested futures or in nested streams. I don't think the compiler will optimize away the field. I don't think it can. Yeah, it probably can't. Okay, so now that my brain no longer hurts, I can say that the two cursor problem is a lot easier to solve in Rust than Python. Okay, that's good to hear. I don't know why. Can you call the next back concurrently? No. So the reason for this is because they both require exclusive access to self, right? So mutable reference, otherwise known as an exclusive reference, you couldn't call next back and next concurrently. You could call one then the other in either order, but you can't have like two threads that both call it because neither of them would have an exclusive reference. And next notably here does not require double-ended iterator, right? There's nothing in this body that calls next back. It does have to access the back iterator, but that back iterator will always be empty if you never called next back. Why do you need to use refmute? I probably don't. Yeah, okay, so the reason for this is that self.frontiter, its type is option of a thing, right? So if I do if let some t is self.frontiter, then t here is like, if I used t, right? The type of t here is t, which is an owned t, right? It's not a reference to. And so this would mean that I have to move out of self.frontiter, but I only have a mutable reference to self, so I'm not allowed to consume this field and take ownership of t. And so I have two ways to get around this. I can even write asmute. So asmute goes from an option t to an option mute t. And now I could get rid of this and it would be fine as mute is this. Because now the thing that I'm moving as a mutable reference as opposed to the t itself, I personally find it easier to write this. So this is saying when you take the t, take it as a mutable reference. It's sort of the opposite of this. We talked about this at length in the previous crust of rust. So I recommend you give that a look. What if instead of live walking, you flatten the entire data into a flattened structure and then iterate over it from both ends? If you did that, you would have to allocate, right? So one thing you could do is in flatten, we just walk the entire iterator and collect everything into a vector. But that means you have to allocate all of that stuff. This flatten will work even if this iterator is infinite because it only consumes element while you're calling next, right? So imagine, in fact, I can give you an example of this. Inf. So here's what I'm gonna do. I'm gonna give you an iterator. That is infinite. So zero dot dot will continue yielding iterators forever. And not only that, but the items of that iterator are also themselves going to be infinite. And this will still work. In fact, this will be a super unhelpful iterator. Like there's no reason to flatten this because this is infinite. So let's make this like one to I. So the inner one is not infinite. That's fine. But you couldn't flatten this into a vector because this is an infinite list. So what is this gonna yield? Well, this is gonna yield. So the first outer item is gonna be a map of, is gonna be this. The second outer item, which is empty, I think. Let's make this zero. So the first inner item is gonna be the range zero to zero, which is empty. The second inner list is gonna be zero to one, which only holds one. So the first thing we expect to get is one. The next, so that exhausts that inner item. So now we go to the next outer item, which is gonna be, so the outer is here. So the next outer is gonna be two, which gives us an inner iterator of zero to two, which gives us zero and one. So next should give zero, next should give one. Just see that sanity check my solution there. Probably wrong. Right, zero, zero, one. Great. And you could keep calling next on this for as long as you want, and flatten will still work. And notice we did not have to allocate an infinite amount of memory, which it would require if you actually flattened it eagerly. So someone pointed out that stream and chat are five minutes out of date. They're not actually, it's just that I'm still catching up with old chat. RefMute is inferred by the compiler in many patterns now. Not all. This is an example where it's not. Yeah, so in fact, here's a different way. The compiler can actually infer this in many cases. So one way you can do this is when you have a pattern, what the compiler is smart enough to do is it will, it will instead of moving, it will mutably borrow. If the thing you're matching on is itself a mutable borrow. So this will also work. I still prefer RefMute myself, but this is equivalent. Next back makes no sense. Yeah, so you couldn't call next back for this infinite iterator. If you tried, the compiler would tell you that next back is not found because this infinite iterator is not double ended. Does this code work for arbitrarily nested iterators? No. If you wanted for a third level, then you would need a front front iter, a front back iter, a back front iter, and a back back iter. Because you'd need to keep track of, you're essentially keeping track of a tree of cursors that you're walking. So this is why it gets pretty complicated to make this sort of generic over the number of levels. I don't know of a way to do that. You could probably have a macro that generates it for you. The way you would probably do this in practice is if you wanted to flatten further levels, you'd basically do some kind of collection. But you can flatten a flattened iterator, right? There's nothing stopping you from writing like, let's do like deep. So this is gonna be a, I guess a vector of vector of vector of zero. And I want the count. I want the count of that to be one. Well, I want the count of this to be two, right? So I don't want to count these. I want to count these. That's what a three level deep would look like. And in practice, the way this would work out is you would flatten the flatten. Think this should work, right? So this iterator, this is going to be an iterator that yields these, and then you flatten that, and that's gonna be an iterator that yields these. Yep, so you can flatten a flatten. Could you nest calls a flatten if it detects more than two levels? No, so keep in mind, none of this happens at runtime. This is a flatten as a compile time construct where you need to give it a type at compile time. It's not like it will just like, it can, there's no way for it to check at runtime how deep the iterator is. This is only written for a two level deep iterator. You could write a, you couldn't even write a macro to do this, I think, because you need to know the type. You might be able to do this with some really fancy type level magic, but you wouldn't generally want to do that. Yeah, and then the other thing I wanted to mention briefly is the standard library also has a method called flat map. And flat map is basically a map over flatten. It's not terribly interesting, but it maps over the outer iterator, not the inner iterator. So rather than look like this, it takes an F whose, let me see if I can. Yeah, so it's gonna require like, um, actually, so it takes an F where F is a function from O item to I where I is the inner iterator and where the inner thing implements into iterator. So this is what flat map looks like. So it maps over the outer iterator and the closure that gets given the outer iterator needs to produce iterators itself, which is a, it's an interesting case study. If you want to try to like see that you actually understood this, try to implement flat map. And then the last thing we have like 10 more minutes or so is it's sort of annoying for flatten to be a freestanding function. Ideally what we want is if you have an iterator, you can just call dot flatten. Of course, you can already do this because flatten is a method on iterator, but imagine that iterator did not have a flatten method and we wanted people to easily be able to use that. What we can do is we can actually use something called an extension trait. So extension traits are not special. They're more of like an idiomatic pattern. So what we're gonna do is we're gonna declare some new trait iterator X for extension and it is only gonna be implemented on iterators. And it's only going to have one method and that's gonna be flatten. It's gonna take self and it's gonna return a flatten of self. There's a lot of flatten in this. That in turn is gonna call the top level flatten function with self. And it's going to require that self item implements into iterator. Ooh, in fact, this is a fun error message to look at. I'm gonna get to that in a second. What this lets you do is if someone uses this trait then, and they have an iterator, actually let's do impo. We do a blanket implementation of this for all T. Iterator X for T where T implements iterator. I'm just writing it out and then I'll explain. So what we're saying here is any type that implements iterator has this iterator extension trait implemented for it and the way you do it is by flattening the iterator itself. And the idea here is that instead of having to call flatten, you can do, let's give it a different name so we don't end up conflicting with the outer one. So our flatten is that you can now do instead of this sort of flatten, flatten business, you can just do dot our flatten. Now this doesn't actually compile and the reason is here we require that self is sized. This is a little complicated to explain why we require that. It says the size for values of type self cannot be noted compilation time, but basically when you have a flatten, it takes a value of type O, a type of type O. A type O, it takes a type O and it stores that O in the struct. The only way you can do that is if it knows how large O is. If O here was like, that's a good example, if O here was the name of a trait, a trait itself does not have a size and so it couldn't store that here in this variable. It wouldn't know what size it has. And so O clearly has to be sized. Size is the trait that Rust uses to express that a type has some known size. And because you can only construct a flatten if O is sized, this complains here that the size for values of type self cannot be noted compile time. Basically self is not sized. Because we've said here that we implement this for any iterator, including maybe if that type is not, including if that type is not sized, any type. So we can just say where self is sized and that takes care of that. And I won't go too much into the details of why this is necessary. Let's see if that works. Self is sized. Ooh, right. And this is implemented for anything that implements iterator, not anything that implements into iterator. And so in order to use the extension trait, you need to already have an into iter. And you already have an iterator. Nice. Yeah, so ignoring the size bit for a second, hopefully this should make sense that we're defining a trait that only has the method we want to add. We say that that trait extends the sort of base trait that we want to provide an implementation for any implementer of. And then we provide that general implementation, that blanket implementation for any T that meets our bounds. All right, I think that's all I wanted to cover for flatten. Let's do questions before we end, though. Fn or fnmute, fnmute, yeah. You could then implement flatten as outer, flat map, inner. Yes, that's correct. Why not implement the monad laws that seems outside the scope of this particular stream? The size bounds is required because you, well, both because you take self here and because you return, you name the self type. Why do I sometimes see a question mark sized? So by default, any generic type must be sized because you might store it. But imagine that you store something like a box O. It turns out here, the compiler can store things that it doesn't even know the size of. And you can say that for this, you can sort of opt out of the implicit size requirement by saying question mark sized. So that's what that means. Could you put the bounds on the trait iterator x instead? You could. So you can do this. Oops, sized. That also works. These are mostly equivalent, but not entirely equivalent. Plus sized. T is implicitly sized here. So that works. All right, great. I think that means we're all done. I'm gonna go turn on the air conditioning again. Thank you for watching. Hopefully that was useful and just follow me on Twitter and you'll get to learn about upcoming episodes. Thanks everyone. Thanks for watching. Hope you learned something. And I'll see you next time.