 Num, num, num, num. Folks, welcome back. So this is gonna be a crust of Rust stream, which if you haven't watched any of these before, these are streams where I try to tackle sort of odd little bits of Rust that are often just covered sort of while you're writing code, you've run into some weird corner case, or they're just a topic that many people have opinions about or are confused about, but there aren't good sort of thorough examples or walkthroughs of how they work. And in this particular stream, what I'm gonna be covering is subtyping and variants. Now, subtyping and variants are sort of, it's a very niche topic. It's something that you probably don't know that you've run into, but it sometimes comes back to bite you and if you don't know what to look out for, it might be really surprising. And it's something that's a little bit hard, I think, to get across in text. So the page I have opened here is the Rust reference. This is like the official Rust language reference, which is still being worked on, like it doesn't have all the details that it sort of needs to. It has a chapter on subtyping and variants and we're gonna be referring a little bit to what's in here, especially this table. But it's not great for understanding what subtyping is and what variants is and what it means for your code. There's also the Rust Nomicon and the Rust Nomicon has sort of a chapter on subtyping and variants as well. And this one is very helpful. I can highly recommend going through that if you're curious about this. But even after reading this, I wasn't quite clear on like, how does this work? What does it mean for me in practice and where is this useful? Similarly, there is a really good, I guess, code file, like educational code file going around that has sort of a very thorough example in code of how lifetime variance works out in practice that goes through just sort of a step-by-step buildup example where you get into why variance matters. And this is also very good. Like if this is something you really want to understand, I encourage you to read through these resources. But as a part of this stream, what I wanted to do was sort of take a little bit of a more pragmatic approach of why should you care? Like where does this come up in the code that you might write? And to get to that, what we're gonna do is we're gonna implement a function that comes from C plus plus and C, which is the stir-talk function. Stir-talk is a little weird, but its functionality is pretty straightforward. It takes a string and you give it a delimiter. Think like you take a string and a character. And what it's gonna do is it's going to return to you the string up to the next delimiter and then it's gonna change the string you gave it to remove that prefix that it just returned. So you can sort of think of this as like similar to strip prefix in the Rust standard library, except that you do a delimiter instead of a string. You can think of it as like the split function on strings that gives you like an iterator over all the things that have been split. It's sort of like that, but different in that it doesn't give you an iterator, it mutates the string in place so that you can keep calling this function. And we're gonna see what we're gonna run into when we try to implement this function in sort of the straightforward way is that variance is gonna make it really hard to use this function. And we're gonna look at how you might go about solving that. This is gonna be a little bit of a weird stream for two reasons. The first is that variance is not something that I've used a lot myself. So it could be that I get really confused over the course of this stream, in which case we're sort of gonna have to do some debugging and figure out like what's actually going on under the hood. And the second is that this is a very like technical, formal topic. And I might be using some of the wrong words at times. I'll try to stick to what I think the right terms are, but some of it can get pretty confusing. There's a lot of terminology there. There's gonna be a bunch of lifetimes flowing around. So please ask questions as we go. If you're like, I don't understand why this is important, why this matters, why we get this error, then please say so in chat and I'll try to get back to it. Because I think what tends to happen in these cases is that a lot of people have what they think are stupid or simple questions, but in practice, a lot of people have the same question. So someone needs to ask them so that I can know to address them. So with that, let's dive into stir talk. So let's do a cargo new lib stir talk. We don't need, I don't want that. Thank you. So let's, we can keep the test somewhere below screen for now. So the stir talk function takes a string. So let's just do this for now. We're gonna add to this over time and it takes a delimiter, which in this case, we're just gonna have be a character. In the real stir talk takes like a list of the limiters, I think in the delim each delimiter, I think can be a string. It doesn't have to be a car, but for the illustration purposes here, this is gonna be good enough. And we're gonna return is a string at sort of the basic signature of stir talk. Now in practice, we're not really taking just a string. We're actually taking a mutable reference to a string. And the reason for this is stir talk is gonna modify. Think of it as like you give it a pointer to a string and it's gonna modify the pointer so that it points later in the string. This is gonna be easier to understand when I write a test later that demonstrates how it works. And they're gonna be lifetimes here and we're just gonna make them all take a for now. And in this case, I'm just gonna have it return an empty string just so that it stops yelling at me. Oh yeah, I'm not sure if it's stir talk or stir toke. It's a good question. Toke is short for token. So maybe it is stir toke. I don't know. That's a good question. See, I feel like a lot of the C standard library should have pronunciation guides. Like it's the same with car. Is it char or car or care? It's a little unclear. Okay, so the way you would use stir talk in practice is you would do something like X is hello world. I'm gonna make it mutable. And then you call stir talk of mute X, let's say space. And then what's gonna be left in X is gonna be world. I guess this needs to use super. Oh, this font size is very large. All right, so camera over text. Oh yeah, you can't, that's fine. Let me go out a little bit here. Let's do that, that should be fine. So this is the way it's gonna be used. Ignore the compiler errors for now. But what you do is you have some string. It doesn't have to be a static string like this, any string. You pass, and X here remember is like a refster, right? Is an ampersand stir. And then we pass a mutable reference to the variable that holds the string and the delimiter. And then what stir talk is gonna do is it's gonna find the first occurrence of the delimiter in the string. It's gonna modify the pointer it got in to start after the delimiter. And then it's gonna return everything up to the delimiter. And so what we get back is hello and what's left in X should be world, right? And in fact, we can write the implementation for it. The implementation is not actually that complicated. What we'll do is if let some i is s find delimiter. So find gives us, I guess does it have to be position? No, I think it's just find. Find returns us the character, the position of the first occurrence of the character we give in, or none if it doesn't exist. In this case, what we wanna do is we wanna split s at that location. So the sort of prefix is gonna be s up to i. And the suffix is gonna be s from just beyond the delimiter and onwards. And then we're gonna say that s is gonna be the suffix and we're gonna return the prefix. So once we write this into code, it's like a little bit easier to understand what's going on. If there is no occurrence of the delimiter at all, then what we're gonna do is we're gonna set s to be the empty string. We're gonna have the prefix be the entirety of s and we're gonna return the prefix. And of course, in this case, because we're returning, we can just make this be this. So the code for this is not actually that, oh, sorry, this should be hello, you're right. So the code for StarTalk is not that complicated. This might be buggy, we'll test it in a second. But the internals of StarTalk aren't gonna be as important for what we're gonna be talking about today. Okay, so does the basics of the function we're writing make sense? Ignoring the fact that currently the test doesn't compile. So it finds the delimiter, it returns what comes before, and it sets the input argument to be what follows the delimiter. So it sort of changes the input argument in place. Basically, the input argument, it becomes an in-out argument, if you're sort of familiar with those kind of semantics from other languages. This will work with UTF-8 as well. Find returns you the character position, ooh, this is gonna be, this is technically should be the length of delimiter. I don't know if there's a, Len UTF-8, great. Yeah, good call. The fun of UTF-8. Yeah, so this should work with UTF-8 strings as well. Like, we're just, we're finding the start of the delimiter and that we're sort of slicing the string up to the delimiter, which should be fine because the delimiter is a normal character. It's not a surrogate. And then we skip past the delimiter. This is the sort of delimiter Len UTF-8. And then we return the suffix, which is everything that comes after that. All right, great. So now we have a thing that works. And we want to test it, right? So let's go back to our test case here. We have a mutable string X, or a mutable string pointer, if you will, X. We pass that X into stir talk with the delimiter of just white space. We assign the return value to be hello. We assert that hello is equal to hello. And then we assert that X is equal to world. All right, so this now gets real weird. We're gonna ignore the fact that stir talk sets null in C. It's not really important here. It does not really really important in the Rust world. So let's actually look at what this compiler message looks at in a little bit more detail. So if I run cargo check, it runs just fine. If I run cargo test, it fails. And let's look at this error message because it's kind of interesting. Let's see if I can make that error fit on screen. So it says, cannot borrow X as immutable because it is also borrowed as mutable. You see the mutable borrows borrow occurs when we take the mutable borrow to X, right? And that mutable borrow of X gets passed into stir talk. And what that does is we've said that hello is gonna have the same lifetime as the mutable reference that's given in. And therefore as long as hello lives, X continues to be mutably borrowed, right? So at here, hello still exists and therefore the compiler gets said because we're trying to use X while it's still mutably borrowed in hello. So this is sort of the standard problem around like having lifetimes. But let's see if we can't do something about this. So let's say that we drop hello. Gee, I don't know if it's gonna let us drop hello and to fix this, it might not. So here's what we're gonna do. We're gonna add a scope around here. So now hello is gonna be dropped. So this mutable reference to X should be no longer in use, right? So why does this not work, right? If I run cargo T, it still fails. It still says cannot borrow X as immutable because it's also borrowed as mutable. And it still points us at the same use. It says that an immutable borrow occurs here and the mutable borrows later used here. So why does it get confused? And remember, I added an extra scope here, right? So the hello borrow here, the lifetime that gets returned, that borrow has gone away. So nothing should be holding on to this mutable borrow of X anymore. X should not be mutably borrowed. And yet for some reason it is. And in fact, I can give you an even simpler way to discover that this is a problem. So let's comment out this test case. Just we'll return to it a little bit later to understand what's going on. But let me give you a simpler example. So we're gonna change stir talk a little bit. We're gonna have it just return a static stir just to sort of get out of this lifetime world for a second. And we're gonna have this just return like empty strings all around. So notice that it still fails to compile. Okay, so this is not related to the lifetime of the return value, right? Clearly because the return value here is the static string. So what's going on? In fact, even if I delete the return value entirely like this, this still doesn't compile. Now it doesn't compile for a different reason. Let's comment out this too. It still doesn't compile. It still complains that X cannot borrow X as immutable because it's also borrowed as mutable. What is going on? There's nothing that should be borrowing X. And the answer here of course, as maybe you've guessed already is variance. Specifically, let's try to work through what the compiler has to do here. So in this test case, let's just get back to this case, right? To simplify the code. So let's see what the compiler has to do. It's trying to call a function that takes a tick a mute tick a stir. Okay, and what we're providing it with is a mutable reference to X, right? That's what we're passing in. And X here, what is X? Well, X is this thing. This thing is a static stir. So it's one of these. Okay, so the compiler sees this and goes, interesting, well, that must mean that tick a is static. Okay, but if tick a is static, then this must be static. And if that's static, then this mutable borrow has to be a static borrow. Okay, but this mutable borrow of X is only valid for the lifetime of X, which is only until the drop here. So this means that this mutable borrow of X needs to be able to last until the end of the program, but X is a stack variable, so that won't work. And then the compiler goes, no, no, the mutable borrow of X lives forever, which means it can't be used immutably here. And in fact, this won't even work, even if I comment this out. So now the code is gonna compile for a second and this might strike you as weird. This might strike you as really weird, right? Why does this now compile? Because the argument I just made still holds, the mutable borrow of X needs to be static. Well, the reason why this now compiles is not because we stopped using X, it's because the compiler now goes, this is a static string, that's true, but what if we just pretended that it wasn't static? What if we pretended that it just lived for the lifetime of it works? Then this becomes it works, this becomes it works, and this becomes it works, or X rather, I guess. Specifically, what the compiler knows is that if you have a static reference, then you can use it in place of anything that is a shorter lifetime reference, right? So if you have a function, if you have a function foo that takes a stir, that take a stir, you can call foo with a static string just fine, right? This is maybe a bad example. Let's do fn main. If I have a X, which is a static stir, and then I have a Y, which is some other stir, and notice it's not generic over the lifetime, it's just let's say it's a string new, and this is a reference to S, right? So clearly Y is a string reference, but it doesn't have a static lifetime. Then I can still here say Y equals X, because if you have a string that has a static lifetime, that's valid in any context that takes a non-static lifetime, because the static lifetime is always longer than any other lifetime, right? This is because of variance. Specifically, the reason why the compiler knows that anything that takes a tick a T, anything that takes a tick a T, you can provide a tick static T instead. It's because of variance. Specifically, this is what's known as covariance. And here I'm gonna introduce a lot of terms to you at once, and I promise you we're eventually gonna get back to stir talk, but this was just motivating why this is a problem. Let me take one step back before I dive into the explanation of why all of this works. To basically get this code to not compile anymore. So here the mutable borrow of X suddenly worked again, right? And that was because the compiler knew that it could shorten the lifetime of this static stir to any shorter lifetime. But we can make it not compile again if we want. Check is static takes a static stir does nothing with it. Then now if I call check is static on X, then now you see it doesn't compile anymore. And because now again, it's not allowed to shorten the lifetime of X because X has to be static. And so now when it gets back to here, it sort of comes back to this conclusion that the mutable borrow must be static, right? This static, static, static, static, static. So to bring all of this then back to variance, if the compiler knows that it can shorten the borrow in this case, right? It knows that it can shorten the borrow of the X here. Then why up in this case does it not also realize that it can shorten the borrow of this? And that's where we're gonna be exploring and like the rest of this video. All right. Why does the assert that I commented out make X need to be static, this assertion? Okay, so let me bring that back before I dive into the variance bits. So if we do this, why is there a problem? Basically what happens is if we don't have this, the compiler can sort of pretend that the, it can pretend that the mutable borrow, that it can make the lifetime of X be the lifetime of this scope. So basically until X is dropped, right? It shortens the lifetime of the stir reference, the static stir to be the lifetime until X is dropped. X is dropped down here. This mutable reference to X, therefore also has to live until X is dropped because the two lifetimes are the same. And when we then do this, this is now trying to immutably access X before X is dropped. And the lifetime is until X is dropped. And so now you have overlapping borrows of X. There's a mutable borrow that goes until the end of the scope and there's an attempt at an immutable borrow here. So that's why this assert makes a difference. Okay, so now that we have this in place, let's talk first about why this example works. So let's sort of rewind to something simpler before we try to explore how we can fix this problem in the first place. So what does this example work? Here, the type of Y, right, is some lifetime tick A, but what we're assigning into it, no, it's not gonna like that, is it? Let me make it a comment, right? Is some lifetime tick A? And yet we're assigning it something that is down here. We're assigning something that is a tick static into something that's a tick A. And this is because static is a subtype of any lifetime tick A. So what is a subtype? Well, a subtype is, okay, and there's formal definitions for this, but I'm gonna go with something relatively informal, which is that some type T is a subtype of some type U if T is at least as useful as U. So T is a subtype of U if T is at least as useful as U. This is not the formal specification, but this is how I remember what subtyping means. So for example, static is a subtype of any tick A because static is at least as useful as tick A. Anywhere where you can take a tick A, a tick static would be just as good, if not better. And in fact, you can use this for things that aren't lifetimes. In Rust, currently variants mostly just affects lifetimes, but you can imagine in a language that had inheritance like Java that let's say that you have some type, you have some class animal and you have some class cat that is a subtype of animal or inherits from animal, right? Here, the property is the same. Cat is a subtype of animal because a cat is at least as useful as some generic animal. You cannot use, so there was a proposal in chat that you can use the wording T can be used anywhere you can be used and that's not true. These are not quite the same and we'll get into that when we get to counter variants later. So this is why I tend to prefer this phrasing. And it's true that like useful here is a sort of weird argument or it's a weird word, right? Cause it's sort of squishy, but think of it as like you can do as much with a T as with a U, right? So you can do as much with a tick static as you can with a tick A, you can do as much with a cat as you can with a generic animal. And this is the example I think that the Rustonomicon also uses. And Rust has this rule around variants, which is that you have three types of variants. You have covariance, you have a contravariance and you have invariance. Most things are covariant. Sure, close enough. Most things are covariant and what that means is if you, let's say that you have some function foo that takes a, let's just go with a T for now. And we're gonna assume that it doesn't return anything. You can use the same argument for if you have some X of type T and you wanna assign to it, these are equivalent. You can call foo with any not animal subtype of cat. No, animal is not a subtype of cat. You can't do, you can't do an animal is not at least as useful as a cat because cats can do things that animals can't. So let's try to work through a specific example of covariance. So let's say that you have a, yeah, I'll give you a counter example soon. So here's what we're gonna do. Let's say that foo takes a take a stir. You're allowed to provide any type that is a subtype of the arguments in this case, right? So I can call foo with some take a stir but I can also call foo with a static stir because static is a subtype of a. So anything that accepts like a take a, you can provide a static instead because it's at least as useful as the type it was expecting to get. Does that basic explanation of covariance make sense? Notice here that I'm not writing let it's generic over a because in that case it's not variance that would happen. It's just that a would be set to static. So I'm sort of imagining that you could name lifetimes and this is some lifetime that's not a generic parameter. This is why in the example I had with main up here I sort of created a why here is a reference to what's inside the string and string is local. So that means that this stir reference has some lifetime that we can't really name, but it's not static and it's not generic. Okay, so now let's think about why is it that not every type is covariant? Why do we have these other types of variants? So yeah, and it's not that foo here is covariant. It's that this argument type is covariant, right? So it's like the reference to string that is a covariant type. This is why this could just as easily be written as X has the type take a stir and you are allowed to say X equals some take a stir but you're also allowed to say that X equals some static stir, right? So it's this type here that is covariant. Ignore the fact that it's a function. So that then brings us to what's an example of something that isn't covariant because a lot of things are, you'll find this when we look at the tables later on too that most things in Rust are covariant. Contravariance is super interesting. So let's say that you have a function. You're writing some function foo and what it's gonna take is an argument of something that is an fn from take a stir to let's say unit or equivalently let X of this type, right? So the question now becomes, what's the variance of this? Because if I now try to provide this with a function, I'm sort of making up syntax here with this like anonymous function like this. Should this compile, right? This is the same syntax as what I used previously but here this gets weird because if you think about the implementation of foo, inside of here the implementation of foo can take, it expect to be given a specific lifetime. Here what I'm, it's expecting to be able to, let's say if we call this bar, right? It's expecting to be able to call bar with something that has some shorter lifetime take a, right? But if I give it this, if I give it a function whose argument is a static string, then really this is a stricter type, right? If it tried to give a function here, if it tried to call the function I passed in with something that only has a lifetime of take a, it wouldn't compile because the function I passed in requires that the type is static. So this gets sort of, this is contra variant. So if you have a, let me see if I can phrase this better. How to phrase this better. foo here expects to be able to use bar with something with a particular lifetime. Here we're providing a function with a different lifetime, but it's a function that takes a lifetime that is longer. So in other words, the caller of the thing we provide has to give a more useful type than it normally would but foo is written to provide a less useful type, a take a. And so this is not, you can't hand foo a more useful function as a good way to phrase it, right? Like you can't hand foo a function that has stricter requirements of its arguments than what you expected. You can provide one that is weaker arguments. So imagine that I had a foo that takes a function that takes a static stir. So this means that this, the body of foo expects to be able, it can provide the argument with a static string. Then it's fine for you to pass in some function that takes an argument that is a shorter lifetime because the caller is gonna provide a more useful lifetime, a static lifetime. So you can think of this as, it sort of flipped on its head to what we talked about earlier. It's contra variant. You need to provide a argument that is less useful, rather than more useful. This is gonna make a little bit more sense if I write it out in, actually let's, this is a good time to look at the reference. So if we go back to the reference, the reference has this variance table where it says that, that lists out a bunch of types and it says what variance does it have in the lifetime and in T we'll get to the distinction of those later. And you'll see here that a function that takes a T is contra variant in T. And it's the only thing that's contra variant. And the argument here, to try to reiterate it again, it's sort of hard to phrase is that if I'm a function or a variable, whatever, I expect you to give me a function that I can provide a short lifetime string to. But you give me a function that requires a long lifetime string. I can't use that function. Normally, that is the case. Normally, if I expect to be given a short lifetime string and you give me a long lifetime string, I'm happy. But if I expect to take a function that takes a short lifetime string and you give me a function that takes a long lifetime string, that's not okay. So in other words, the variance requirement is flipped. When it comes to function arguments. Because actually, here's maybe a way to phrase it that might be helpful. Let's write out these two functions. Let's write out a fn tick static stir and an fn tick a stir. Which of these is more useful? And then let's contrast that with a static stir and a tick a stir. Which of these is more useful? Well, this is more useful, right? This one is easier to use. It has more features. It's more useful. It's more widely applicable. Because any static stir satisfies any tick a stir. Here, which one is more useful? Well, in this case, this one is more useful as in more generally applicable, right? This one has stricter requirements of its color than this one does. This one can take any short lifetime string. It can also take a longer lifetime string, that's fine. But it can take a short lifetime string as well. This one, you must provide a long lifetime string. And so the function that takes a short lifetime string is the more useful one. And so in the subtyping world, a tick static is a subtype of, I think this is the phrasing, the wording that sometimes used is a subtype of any other lifetime tick a. And because references are covariant in their lifetimes, a static T is a subtype of a tick a. But here, even though static is still a subtype of tick a, a function tick a T is a subtype of a static T, right? So notice that the subtyping relationship here is flipped. And that's what contravariance is. Yeah, there was a good comment in chat that this requires its argument to be maximally useful, which means that the type itself is less useful because it has stricter requirements. All right, so that's contravariance. And function argument types are the only place where we have contravariance in Rust today, where you need to provide a, I'm not gonna try to say it again, but it's the only place that we have contravariance in Rust. Which I think gets us to invariance. Okay, so invariance is interesting. So let's look at why we need invariance. Like why is this not enough? Specifically, if we look back at the table here, you'll notice that a mutable reference to a T is covariant in the tick a, but is invariant in the T. This is finicky, but let's try to get at it. Okay, so let's say that I have some function foo. It takes a mutable reference to a cell of static. Actually, it doesn't even have to take a mutable reference. It can take an immutable reference even. All right, actually, let's do, is that how I wanna explain this? I think I wanna explain this with a tick a, yeah. Actually, no, I know how we're gonna do this. Okay, so here we have a function that takes a mutable reference to a string with some lifetime. And let's ignore what the lifetime is on the mutable reference itself. I don't wanna get into cell static quite yet, but maybe later. But let's imagine that you take a, you have a foo that takes a mutable reference to some string pointer. And what this is gonna do is it's gonna, I guess, s, it's gonna set s to be equal to, let's have it also take an x. It's gonna set that equal to x. That's all it's gonna do. All right, so let's imagine that mutable reference were covariant. So in that case, I could have a, this is now a static stir, right? We can all agree on that. If I could do equals string new, so I just wrote some code. Let's talk through what's happening here. So here I have x which is a static string reference and I give it a static string. I create a stack local string and I call foo with a mutable reference to my x and a reference to the stack local variable. If mutable references were covariant, then this would work just fine. The x is a more useful type to static stir. So we provide that more useful type into here and it sort of gets automatically downgraded into a tick A. This is again, if mutable references were covariant, then this would compile just fine, right? Because we would sort of downgrade this tick static into a tick A. Inside here, we assign the thing behind the mutable reference, so this thing, to be this thing. They have the same lifetime, so this compiles just fine. We get back out here and we drop z. So z is now gone and the reference we gave in here is now gone. But the type of x out here is still static. It's still a static stir. But what we made it point to was this tick A which was a pointer into this. So if I now try to like print x, who knows what would happen? x is now a, even though it's supposed to be a static string, a static string reference, it's pointing into stack local memory that has since been dropped. Clearly, this is not okay. This should not compile. And the reason why this doesn't compile is because mutable references are invariant in their argument type. And what that means is if you have invariance, it means that you must provide something that is exactly the same as what was specified. You cannot provide something that's more useful. You cannot provide something that's less useful. So it's not covariant. It's not contra variant. It is invariant. And because of that, this code no longer compiles. And let's walk through why. We have a static string. We have a z, which is a local thing. We construct a reference to z, which is a non-static string. And here, when the compiler gets to this line of foo, it's trying to reconcile the following types. So it's trying to reconcile this signature with this signature, right? These are the provided arguments. And in order to reconcile these, it needs to be able to make static be equal to takeA or takeA equal to static. But it's behind a mutable reference, and mutable references are invariant in their T in the thing that they reference. And therefore, the compiler is not allowed to have these not be equal. Therefore, the code does not compile. And therefore, this problem does not arise. Okay, so does that make sense? Why we need invariance for mutable references? It's basically the sort of informal way to think about it is that if we didn't have invariance for mutable references, you could sort of downgrade a mutable reference to something that is a reference to something less useful, stick in something less useful. But in the outer scope, it's that the thing you took a mutable reference to still has the more useful type. And so now you have something that thinks it's a more useful type, but it's actually a less useful type. And that's not okay, because it doesn't have some of the properties that its type indicates that it should have. So now, now let's figure out why in this table does it say that a mutable reference to T is covariant in its lifetime, but invariant in the T. Why is that? Okay, so the reason why that is, is let's see here, how are we gonna explain this? Mute Z is string new, X is gonna be a mutable reference to Z. Or just a Z. I don't really have a, actually, here's what I'll do. A, B. Actually, this I can do in a function foo. Just a sanity check that I'm doing the right thing. That's it. B. V. Ah, I'm just, I'm trying to find the right way to phrase this in such a way that it's not too confusing. So here the lifetime of the mutable reference to B is the lifetime from B until when B gets dropped. Okay, yeah, this will work. And this will, why will it, why will it work either way? That's annoying. Ah, so that's not a good example. Oh, I have a better example. Let's say that we have a foo that takes a, oh, this is a much better example. What am I doing? It takes a mutable reference to, let's say a, what's a good example of this? Let's just use a bool, because it doesn't really matter. Takes a mutable reference to a bool with some lifetime TK. Ah, fine, close enough. And I'm gonna make a mutable reference that has a static lifetime. So I'm gonna do box new true. Then I'm gonna say box leak X. So box leak is a way to introduce a memory leak in a safe way. So you can just leak, if you never drop the box, then the thing inside the box lives on the heap forever because it's never gonna get dropped. So you get a mutable reference with any lifetime, but specifically with a static lifetime. So X now is a static mute bool. And now I'm gonna make, I don't even need foo. Now I'm gonna have a Z, which is a bool. I'm gonna have a, oops, true. And I'm gonna have, ah, I'm gonna have Z, sorry, I'm writing the code while I'm thinking of it. And then I promise I will explain it after. I'm gonna have a Z, which is gonna be a mutable reference to Y. And I'm gonna set Z equals X. Okay. So here, this compiles. That's fine, drop Z. I just wanna get rid of some of the warnings to make it easier to under, oops, nope. Pub, and stop compelling compiler. Great. All right, so here's what we have. We have a local stack variable Y. So this, and then we take a mutable reference to it. So this mutable reference is effectively gonna have, this is gonna be sort of a tick Y, I guess, mute bool. That's the type of Z. Then I create X, and X is a static mute bool. And you can ignore this line. And down here, and you can also ignore this line. They're just to get rid of compiler warnings. And here, what I do is I do Z equals X, right? So on this line, I'm taking a tick Y mute bool. And I'm assigning it a tick static mute bool. So why is this okay, right? Like we said mutable references are invariant. So why is this okay? Well, mutable references are invariant in the type that they point to, but they're not invariant in their lifetime. They're covariant in their lifetime. And so here, the compiler tries to reconcile these two and goes, yeah, static is a subtype of Y. And therefore, this assignment is fine. And this is the reason why mutable references are invariant in their T, but covariant in their lifetime. Because this isn't a problem. And you can't use this to pull the same kind of shenanigans we were trying to pull earlier. Because even if you assigned through this, you can't accidentally assign like a less useful type into what's behind the mutable reference because that part is still invariant. Basically, what this is saying is, it's okay to shorten the lifetime of a mutable borrow, which sort of intuitively makes sense. Like that's fine. So does this roughly make sense? For how immutable references are invariant in their T, but covariant in their lifetime. I know, there's a lot of stuff we're processing here and it's fairly involved, but hopefully it makes sense. All right, so now that we've gone through all of this and subtyping and variants, let's now go back all the way up to our stir talk to try to figure out what's going on. So now that we look back at this, right? We're gonna have this return our take a stir again all the way back to the top. The lifetime is any with leak. I think it's generic over the return lifetime, but it could just as well be static because mutable references are covariant in their lifetimes. Okay, so we're back to our stir talk right here. And now we want to figure out how we can make this code work. And now when we look back at this error, we might actually be able to start to make sense of what's going on. We get the compiler tells us that X does not live long enough. And that's because when we looked, let me bring this back. So let's make these a again. So the argument to stir talk is take a take a. The, what we're providing is a take a static stir. So the compiler goes, this must be static, right? The function is generic in take a. So it's allowed to sort of replace this one here. This is not subtyping. This is just take a must be static, which means this must be static. And now what the compiler has to do is it has to figure out how long is the mutable borrow going to be? Well, the mutable borrow has to live for, okay, so this is where the circle gets completed. We know that mutable references are covariant in their lifetime. So in theory, this could just remain take a, where take a is like sort of take X or something, like the lifetime of the borrow of X. But we also know from the function signature that this lifetime and this lifetime must be the same. So if this was take X, then this would have to be take X, right? But we're not allowed to shorten the lifetime of what's behind the mutable reference because mutable references are invariant in their arguments. So we can't shorten this to X. If we could, right? If we could shorten this, then this take a would be X, this take, which would make this take a X, which would allow this to be X. But we're not allowed, if we put this back to take a to sort of indicate that it's generic, right? What we want to do is this, where X is the borrow of X. But in reality, we're in this position and we're not allowed to sort of weaken this lifetime reference. We're not allowed to make that shorter to be X to make the function signature match what we want. Because this is behind sort of the invariance of the mutable reference. The only thing we're allowed to do, and again, because mutable references are covariant, is that we can make this static. And that's what the compiler does. But if it makes it static, it goes, well, the mutable borrow of X isn't static because X lives on the stack. And so it goes away right here. And so X does not live long enough. So that's how we get, that's why the compiler ends up with this particular error message. Okay, does the explanation for why this doesn't compile make sense? I'll get to how we fix it in a second. The fix is like stupidly simple, but it's important how we get there. Because it's easy to just throw lifetimes everywhere. But I want people to understand why and that's why we're doing this stream. Okay, so the solution to this, as some have already pointed out in chat, is to introduce an additional lifetime. Right? So let's go back up to the signature of StarTalk. We can have this have two lifetimes, which I think means that now you can't see this. So let me add some new lines so I can bring it down here. So now we are gonna have two lifetimes. And we're gonna do this. So what we've now done is say that StarTalk is generic over two lifetimes. One that is the mutable borrow and one that is the string that's being pointed to. And the return type of course is the string being pointed to because that's where we're returning a reference into. And what we're doing here is making it so that the compiler is allowed to choose these two separately when calling the function rather than having to pick one and stick with it for both. And now it doesn't matter so much that the T behind the mutable reference is invariant because the compiler can choose these two lifetimes independently, they're generic parameters. So we can choose this to be static and this to be non-static. And in fact, with this signature, go back here, now this code compiles. And if we, in fact, go back to our earlier example, the one we wrote in the very, very beginning, now this compiles as well. In fact, I think we can even remove this. And I mean, just to check that it works, this now passes. So the test runs, it compiles. Our implementation was correct, which is amazing. And the reason to go over just that one last time is that we have a signature that says stir talk, it takes a tick a tick b, tick a mutic b stir and returns a tick b stir. And we're trying to call stir talk with with tick b being static. So if tick b is static, then the compiler's just gonna accept that that is the case because it's generic over b. So it replaces every occurrence of b with the lifetime we provided for b. And this tick a is gonna be the borrow of x that we made. And so the compiler injects that for every case of a. It doesn't have to use subtyping at all. In fact, there is one place where subtyping comes in here, which is kind of, I might be lying here, but I think it's true, which is that here when we borrow x, right, that borrow sort of theoretically is like borrowing x for the remainder of the function. Like why when I say, if I say, in fact, here's a better example, z is at mutex. Why does this code still compile? z borrows x and z gets dropped at the end of the function. So why is this okay? Why doesn't it complain that I'm trying to mutably borrow x again here? It doesn't complain because z isn't used beyond this line and mutable references are covariant in their lifetimes. So it's allowed to shorten the borrow of x here so that that borrow stops here, right? So essentially it goes from a sort of tick x mute to a tick until z mute, where this is until z. It's allowed to do this shortening because mutable references are covariant in their lifetimes. And at this point, if the z borrow stops here, then it's fine to start another borrow here. So let's see. I'm just parsing through chat for questions. Why isn't a tick be... So some people are asking, why is this bound not needed? I need to make my video smaller or something, but I think you know that it's a stir there. So why is this not needed? Or some variant of it, pun not intended. This is not needed because it's always assumed that lifetimes are at least for the duration of the call. Like this is just an assumption that the Rust compiler makes that any generic lifetime argument implies at least for as long as the duration of the function. That's all we need for this mutable reference, right? Like when you execute the code in this function, it only assumes that this mutable reference exists for the duration of the function. It doesn't care what happens after. If it gets dropped immediately after, that doesn't make the function body any less correct. And tick be doesn't really depend on tick a, right? All we're doing is returning something with the same lifetime as what was passed in. And that doesn't have anything to do with how long the pointer we were modifying lives for. So there doesn't have to be an association between tick be and tick a here. Would you be able to modify Z after stir talk? No. So if you hear said Z equals foo, this would no longer compile because the borrow of X lasts until here. And that overlaps with the mutable borrow here. Would your last example still be true for the previous addition? Something seems to be tied to nonlinear lifetimes here. I think you're right. I think that in the 2015 edition, although the 2015 edition of Rust has NLL enabled now, I believe, but before we had non-lexical lifetimes NLL, this code would not compile because it wouldn't know to shorten this borrow. After the explanation thinking it's a general rule that tick, okay, so the question in chat is for, this question, isn't this a requirement in order to have this in the first place? And I don't think that's true. Let's sort of work through the cases, right? So can you have a mutable reference to a value that has a shorter lifetime? Or let's do it the other way around. Can you have a mutable reference to something that lives longer than the mutable reference? Well, that's obviously true, right? In fact, our mutable borrow x here is a good example. The x lives shorter than the lifetime of the things that it lives for. So yes, you can have that just fine. The opposite, I think you're right, that can't exist. So you can't have a tick a mute, or just tick a, tick b, t where tick a is longer than tick b. Because I don't think you could construct such a value in the first place. The argument for why this can't exist in the first place is because how would you, what would happen when the b lifetime ends? Well, you still have this reference around, but this lifetime has ended. So what even is this reference? I think in practice, rust would be fine with this because the lifetimes are covariant. So it would shorten the tick a, tick b to whichever lifetime is shorter. I don't think it actually implicitly adds a bound that tick b must be longer than tick a, or that tick b must be a subtype of tick a. I don't think it adds that bounds implicitly. I think it's just the, if you ever tried to violate it, you would get a compiler error. And so this code up here, it doesn't need the bound, but if we ever tried to do something that made that assumption, then the compiler would yell at you. But it's just really hard to write code that would make such an assumption. If it tried to use the outer reference after the inner reference became invalid, that's when the compiler would scream at you. Okay, so now we've been all the way through variance and subtyping. And let's now get to one last thing I wanna talk about, which does come up occasionally here, which is like, you might have seen the following type signature or phantom data. So you might have seen this and you might have seen this. And you might have wondered, well, what's the difference? Why do some things write this while other things write this? And why do people do this in the first place? Well, imagine, so the reason why you use phantom data in general is you might have a type that is generic over T, but doesn't directly contain a T. This comes up often if you do something with FFI, if you do some kind of, what's an example of this? Let's say that you are like deserializing something. As you want a deserializer that is generic over the type it's going to deserialize. So deserializer, but it doesn't actually contain a T. It's just that you want the deserializer to know which types to produce. And so in that case, there's like no T in here because we haven't deserialized one yet, but we do want it to be generic over T. The Rust compiler will complain if you do this. This is an error because it says that the parameter T is never used and that's not okay. And so it suggests you add a phantom data, right? So you add a field that's a phantom data and a phantom data is the only type in Rust that is generic over a type parameter but doesn't contain that type parameter. And so people write this. But then some people write this instead. So deserializer too. In fact, if you look inside SERDI, I think, you'll see this signature. And the question becomes why this and not this. And sometimes you'll even see like, you'll even see this. And the reason here is because of something called the drop check, which is when a type is dropped, Rust needs to know whether the generic types might be dropped as well. Normally this is the case, right? If you have a vector of T's, then when you drop the vector, you're also gonna drop some T's. The reason that is important, and this is sort of not really getting to vary in specifically, but it is, I'll get to how it's relevant. So let's say I have Z, which is a vector of let X equals true. And now I drop Z and then I print X. Ooh, that's a bad example. This is a better example. So this should clearly not be okay, right? So I have an X, which is a string. I have a vector that contains a reference into that string. I drop X and then I drop Z. The reason this is not okay is because when I drop the vector, it might end up accessing the things. Well, when I drop X, that's not okay because there's still a reference that lives to it down here. But what if Z is never accessed again, like this? Z is never accessed again. And because Z is never accessed again, it's actually okay to do this, right? I drop the X, this reference still exists, but it's never used beyond this point. And how does the compiler know that this is fine? Because this wouldn't be fine if I had, let's say, a touch drop, impole, it requires that its type is format debug. And we're going to impole drop for touch drop. This is beyond what we were planning to talk about, but it's an interesting enough problem that I want to touch on it, mute self. And it's actually going to print out the thing that it's about to drop. T. So if I do here, if I make this a touch drop of this instead, this code no longer compiles, right? So if this is just a reference, it compiles. If I make it a touch drop that contains the reference, it no longer compiles. And this is what we want to happen, right? Because here, when Z is dropped, right? Down here, there's sort of implicit drop of Z. When Z is dropped, we're going to call this drop implementation of touch drop, which is going to access the inner type, which means it's going to access this reference. So dropping X here is not okay, because when Z gets dropped, it's going to try to access the reference. But this, here, when we drop the vector, dropping the reference does not access the inner type on drop. And somehow the compiler has to know this. And it does this through something called the drop check, which is basically, it needs to know whether the, for vector, for example, it needs to know whether the vector drops its generic parameters or not, right? Vec is generic over the inner type. And Vec will not, Vec will only drop inner types if they implement drop themselves. And the way that the compiler knows this is, it looks at the generic parameters and it sees whether the type contains a T. This is, I'm kind of lying. There's an unstable nightly feature in here. But there are, basically, it looks at whether the type contains a T. And a phantom data T contains a T as far as the type system is concerned. It's considered as dropping a T. This though, this signature, does not own a T so it can't drop a T. If you drop one of these, you're just dropping a function definition, which won't drop any T's. So that's why this is a preference compared to this. If you know that your type doesn't drop a T, like this is the case for a deserializer. It doesn't own a T, therefore it doesn't drop a T. So we want to allow something like a touch drop because we know that we're not gonna call drop on the inner type. If you had a deserializer of touch drop, then it's fine to drop the deserializer if the reference inside the touch drop is now invalid because you're not gonna drop any touch drops when you drop the deserializer. It doesn't contain any. It's a little involved. So you might wonder, well, why not just use this instead? Aren't these equivalent? And no, they're not. This one is covariant. This one is contravariant. And so if you did this, you would end up with this type being really annoying to use because it wouldn't be covariant. It wouldn't be able to shorten lifetimes. So you wouldn't be able to provide a static in place of a Tic A. Whereas with this, you can. So that's why you often see this function signature or this type signature in types that don't contain T's. Very roundabout. But all right, did that roughly make sense? I know that was involved, but hopefully it was someone's interesting. It's also a little bit of a simplification. Technically here, there's a made dangle. I think it's called that I haven't talked about, but I'm gonna ignore it because we've run for about an hour and a half and this is roughly what I wanted to talk about. Made dangle is a really interesting property. I recommend you look up. If you find this sort of stuff interesting, I recommend you look it up, but it's not something we're gonna touch on here. What is self.zero? Oh, self.zero is, touch drop is a tuple struct with one element. Self.zero is accessing the first element of the tuple. Sorry, yeah, you're right. The made dangle is in drop here. It's like here or something. I think it's not even in, is it in the drop implementation? It's not really important for the distinction I wanted to make. Wouldn't the compiler know that the only T inside a DC realizer is a phantom data, which doesn't actually hold the T? It could, but it's actually better for it to assume that it's gonna drop a T. Imagine the DC realizer holds like a, like a void, like a C void pointer. Oh, which is a thing like this. And this is a pointer like into C land. And so there's no T stored in this, but imagine that this pointer is actually a pointer to a T that was allocated in the world of C. And so you actually do have a drop that like calls into like C free self C void. So you do actually drop a T when this type is dropped. So you want phantom data T to communicate that you are owning a T and might drop one. That is sort of the semantics that you want. And so that's why the phantom data T is sort of thought of as holding a T even though you're not. So yeah, you're right. Phantom data F and T is a marker of contravariance. You very rarely see this. One case where you do see it is imagine that you want your type to be invariant in T. There are a couple of cases where this come up where you might have some interior immutability. And so you need to have invariance so that people can't pull the same hack as we looked at for immutable references. The one thing you can do is you can do this. I guess let's make this a four and have this continue to be here. So D serializer four is invariant in T because it contains, it has to be both covariant and contravariant in T, which no type can be both. Variance is one of these three. So in this case, the compiler concludes the T that D serializer four must be invariant in T. And you might be like, well, can't you just do this instead? You could, but the problem is this now requires a lifetime. So you would have to add a lifetime to your type, even though it's in a phantom data, which you probably didn't want. You can also do this. That's the other way to get invariance for a T with a single field. And indeed, the other way to express this thing is a star cons T. Star cons, oops, both of these are covariant in T. And this one's shorter, but people get scared when they see raw pointers. So I think that's why people prefer this one. And in fact, if we go back to the table, right, you can use this table to figure out which type should I use. So star cons T is covariant, star mute T is invariant, and F and T is covariant as we looked at. You'll see that unsafe cell is invariant and that's because an unsafe cell lets you mutate the T. And therefore, for the same reasons why a mutable reference has to be invariant, unsafe cell has to be invariant. So I don't know whether it's preferred to use this over this, that they're equivalent, but this is generally nicer because once you introduce raw pointers, there are a couple of things that you don't get. I think send and sync are not auto-implemented for raw pointers. And therefore, if you have a type that contains a phantom data that contains a star cons T, your outer type would not be send and sync. Normally send and sync are auto-implemented for your types, assuming all the members are. But if you use this type in a phantom data, it wouldn't be. So that's probably why people prefer this one to get those auto-implements. Could you just say f and T to T for invariance? Yeah, I think you can. This signature has to be contrarvariant in T, but also covariant in T, and is therefore invariant in T. I think that's right. And yeah, like variance and subtyping is something that like, it's rare that you have to deal with it. Usually it comes up in two contexts. One is maybe your type by default becomes invariant, but you know that it's covariant, and so you can improve the ergonomics of your type by making it covariant. In this case, you have to think really carefully about whether it really is covariant, because if the compiler automatically concludes that your type is say invariant in T, it was like generic over T, and the compiler concludes it's invariant over T, then if you try to sort of tweak things to make it covariant, you have to make sure that you're not vulnerable to this kind of sort of invariance attack, if you will, of sort of this accidentally weakening, and that can be hard. The other case is in unsafe code, it might be that the safety of your code relies on a generic parameter being invariant, but the compiler concludes your type is covariant. So let's say you have some type, you have some type foo is generic over T, and let's say that all it really contains is a star cons T. The compiler concludes that this type is covariant in T, but let's say that you have some code deep inside foo that for some reason allows you to mutate somewhere. This can't actually happen in this case because you're not allowed to mutate through a star cons, but it's a separate discussion, but imagine that your unsafe code ends up mutating the T somehow, then now you have to make sure that your type is invariant in T, rather than the covariance the compiler assumes. In general, this shouldn't happen because in order to mutate, basically invariance is only important for mutation. You can only mutate through either a mute T. There are only really three types you can mutate through. A mute T, ref T or an unsafe cell T, and all of these are invariant. So you would have to work really hard to get something that's actually safe, like it's not undefined behavior, but also the compiler's conclusion about covariance is incorrect. But I think I've seen one case of this. Do four Tick A changes, does four Tick A change any variance or it doesn't have any relationship? So the question is about if you have this syntax, this comes up in like where four Tick A, like T implements debug or something. So this is a special syntax you can use if you want to say that for any lifetime, this bound holds. I think you can only use it in bounds and this doesn't affect variance at all. This is just saying that this must be true for any lifetime Tick A. So there's no variance or subtyping involved here. You can mutate through star cons T if you cast it without UB. Yeah, so this is why I said like there are cases where you can do this, which is a star cons T you can technically turn into a star mute T which you can then turn into a mute T which lets you mutate. Generally anything that gives you a star cons T does so because you ultimately went from a immutable reference and not a mutable reference and this is undefined behavior. You are never allowed to mutate through a pointer you got from a shared reference. This is never allowed. So that's why I'm saying that you really have to contort yourself. It has to be like you got a mute T you turn it into a mutable pointer to T which you turn into a cons T which you turn into a mute T which like this pattern is okay. But this pattern is not. The first one is always undefined behavior. The second one is technically not it's not undefined behavior. And this is one way in which you might end up with covariance because you stored this thing but you need invariance because you're accessing this thing. I think Vect T and Box T contain a unique T not a cons T which and unique contains a non null. And in fact, we can look at the non null type. Oh, I think I have light theme on sorry. So the non null type is interesting. The non null pointer type is a star mute T that is non zero and covariant. So this is a covariant mutable pointer type. And this one you have to be really careful about. So you see how this in the documentation it says unlike star mute T non null is covariant over T if this is incorrect for your use case you should include some phantom data in your type to provide invariance such as the examples we looked at. You have phantom data cell T is another way to get invariance without introducing a lifetime. But yeah, you do have to be a little bit careful about using this type because if you're not careful someone might then be able to pull the mutability invariance trick on you. There's non null. Yeah, non null contains a const T, you're right. And this like, this is one of the few cases where you might be able to mutate through a const T safely. The question becomes how did you if you look at, I think if you look at non null the constructor for it. Yeah, the constructor for it takes a mute T not a const T. And the idea here is to prevent you from accidentally doing this by going via a non null because this is still undefined behavior. But this is something you might get at through a non null. Okay, I think that's everything I wanted to cover today. And notice that we started out with like a super simple function like stir talk is what, 10 lines of code. And if you think about it like if you started out with this code, right? Or maybe you, yeah, this is like you started out writing no lifetimes and the compiler yells at you, right? So if we do this, this is equivalent but it says missing lifetime specifier because it assumes that every reference has its own lifetime. So you're like, all right fine I have to add a lifetime and you just like stick TK on all the things then you very easily end up with this problem without realizing why you cost this problem in the first place. This function is just like not possible to call as we saw with the very beginning we tried to write a test case in the most straightforward way for this function and it wouldn't even compile. And if you get stuck in that you're like, why? How did this go wrong? And of course probably what you would do if you ran into the situation might be like, oh, I'll just do this. And then you're like, oh it works and then you move on with your life. And it's true that that is the solution but hopefully over the course of this video it's clear why that's the solution and why it was a problem when you didn't have the two lifetimes. Whew. All right, I hope that now you understand variance and subtyping a little bit better than you did when we started. I certainly do. Yeah, the other thing, this is a good point. The way I would actually probably write this is this. So we have in Rust today the ability to allied lifetimes so you can write tick underscore. You can write this too but the compiler and the compiler doesn't yell at you but I want to stop all of this code below here from compiling. I don't like it. Go away. But I'll keep my test case. Just wanna demonstrate the last thought I'll have here. So you're allowed to write this and it works just fine. But if you turn on Rust 2018 idioms which I recommend you always turn on for warnings you should get a warning here. Oh, it's only for return types. Actually, you're right. So this is fine. I tend to like to be explicit about things having different lifetimes so you can write down this tick underscore. Regardless, it means the same thing. If you have a reference that doesn't have a lifetime associated with it Rust automatically generates a distinct unique lifetime for each reference. And so this is equivalent to saying that there's some additional tick B and that gets assigned here. So realistically, I think the way I would write this is this instead. Because the caller never really needs to think about this additional lifetime. All right, sweet. I think with that, that's all I wanted to cover. Thanks for sticking with me. Thanks for all the questions. I know it's a journey to get through this. And I know it's like hard to explain sometimes because all of it is very circular and trying to like speculate what the compiler is doing and the type inference is doing. But I hope ultimately we landed in a good place. Thanks for joining. I'll see you all next time. I'm hoping that the next stream is gonna be a live coding stream. I want to do some kind of larger port again. I think doing ports are really fun. I don't know what I'm gonna port yet but I'll announce once I do. And that'll probably be a longer like a five, six hour reporting stream again. We'll see. See you next time everyone. Bye.