CoRecursive: Coding Stories - Tech Talk: Moves and Borrowing In Rust With Jim Blandy

Episode Date: July 3, 2018

Tech Talks are in-depth technical discussions. The surprising thing about rust is how memory management works.  Rust has the concepts of moves and borrowing.  If you have heard about Rust, you may h...ave heard people talking about the borrow checker and trying to make it happy. In this interview, Jim Blandy walks us through what these concepts mean and how they work.  We also talk about how to avoid fighting with the borrow checker at all and why the conceptual model that Rust adopts, though it may seem unusual at first, is actually more representative of how computers work and therefore an easier programming model.

Transcript
Discussion (0)
Starting point is 00:00:00 Welcome to Code Recursive, where we bring you discussions with thought leaders in the world of software development. I am Adam, your host. Basically, in my experience, and I've talked to some friends of mine about this, it really is something that you can internalize just as well as the other things. And it really does correspond to the costs of the computation you're doing in a sort of a better fit than the C++ world. C++ is trying to give you the illusion of a value-oriented world where assignment just means that you've got a copy. But that's not actually the world that we live in. We live in a world where we care about memory, where we care about time spent copying things.
Starting point is 00:00:52 The mismatch with reality is actually on C++'s side here. And what Rust is doing is it's simply taking the real costs that you are always paying and making them show up in the type system. Hey, this is the second interview with Jim Blandy, co-author of Programming Rust. Today, we get into the details of borrowing and ownership. If you haven't listened to the first interview, you should probably check it out first. Today, we get down into the actual weeds of how Rust handles ownership and borrowing and how it's different from other languages. There's also at least one joke about linear algebra that went way over my head.
Starting point is 00:01:34 Enjoy. So, Jim, thanks for coming back to the podcast. Yeah, thanks for having me. So last time, I think you did a really good job of explaining, I think, why Rust is important and what job it's trying to solve. And for me, to kind of understand how that actually works, I think it's great to actually talk about examples and see how that works in practice down at the actual level of code and memory mapping. So yeah, I want to dive into some of that today. I feel like this is becoming maybe the world's smallest book club where I just bring you on and ask you questions as I read the book. Yeah. Well, as long as people have a good time, that's fine. First of all, I've made it further through the book and the book now has a coffee stain on it, which I think shows that I've actually like that.'s how you know it's going to be used.
Starting point is 00:02:27 It's a mark of honor. Yeah, it's got to be like frayed a little bit. And somebody put something on Twitter that shows the picture of their kid drooling on it. And I think that's pretty good too. So, yeah, for our listeners, if you haven't listened to the first episode, I recommend you go back to it and we'll just jump in with the specifics. So memory management strategies. What I'm used to pre-reading this Rust book is there's languages like C, where you kind of have to free the memory yourself. And then where I spend most of my time, which is in garbage collector languages, where the freeing just happens and I don't even worry about it.
Starting point is 00:03:05 It's like a runtime property. So where does Rust fit in this kind of spectrum? Okay, so Rust actually sort of threads a third way between those two extremes, right? The premise, both C and the rest of the languages, both C and C++ versus the rest of the language, they both have the same kind of problem, which is that you want to make sure that the thing that's getting pointed to outlives the pointer. And there are actually two ways to phrase that. You can say the thing that you can say that the referent has to outlive the pointer, or you can say that the pointer must not outlive the referent, right? And so the way that almost
Starting point is 00:03:55 every language solves this is by saying, look, we're just going to periodically scan all of your memory and find every pointer that could possibly be used and make sure that we only recycle the things that are never pointed to, right? And that's garbage collection, right? And garbage collection has this really nice property, which is that, you know, it guarantees that you never have a pointer that's pointing to memory that's been recycled. And that's essential for security. And Java was the language where garbage collection went mainstream. I mean, Lisp had it from the beginning. But until that point, people were kind of uneasy about it, and they weren't sure whether garbage collection was suitable for use in a serious systems programming language. But then Java showed that it was something that could really be made workable.
Starting point is 00:04:47 And one of the reasons was they wanted Java. Java was designed to be a language that could be shipped across the network and it could run on your web browser. It could run on a handheld device. And so they needed that security property and garbage collection is the way to satisfy it. Now, the problem with that approach is that the way it goes wrong is if you have a pointer sitting around to something that you've forgotten about, then even if your program is never actually going to use it again, the garbage collector can't tell that or often can't tell that.
Starting point is 00:05:26 And the garbage collector will blindly retain everything that that pointer points to and anything that can be reached from that pointer. So if you've got some, you know, like what happens in JavaScript all the time is you'll set an event handler on something, right? And you'll forget about it. I mean, event handlers in JavaScript are kind of like, it's really easy to sort of lose track of them. And so you've got an event handler sitting around, and the event is never going to happen.
Starting point is 00:05:50 And it's pointing to a closure that it's going to run if this event ever happens. And then that closure happens to have closed over some other variable that you weren't expecting. And suddenly, the next thing you know, you've entrained the entire text of the Encyclopedia Britannica, right? And it'll never go away. And you don't know this. And so there are tools in JavaScript for saying, hey, who is holding onto this? Why is this thing reachable? That the modern web developer tools provide ways to answer questions like that because that's the kind of problem you run into. You run into what amounts to leaks, right?
Starting point is 00:06:29 But leaks are safer than dangling pointers. In C++, the programmer gets to decide when something gets freed, right? And what that means is that if they are wrong, if then they free something while that, you know, this unexpected pointer is still around, then that pointer is now sort of a live wire. It's a bomb waiting to go off. And you end up with, you know, crashes or security holes caused by people accidentally using pointers to memory that's been recycled. And so there's this tension, right? One, you want to make sure that the language is trustworthy, right? And you want to keep objects around for a long time. You want to keep objects around
Starting point is 00:07:17 as long as they could be used. But then two, you want to leave the programmer in charge of how much memory they're using, right? If the programmer believes that their copy of the Encyclopedia Britannica is no longer necessary, it should go away at that point, right? And so basically C and C++ give you the second property, right? That you are in control of when memory gets used and when it gets freed. And basically all the other languages fall on the side of safety. And then the failure mode is that you don't get dangling pointers, but the failure mode is that you get leaks. And so neither of those compromises are acceptable for Rust. Rust
Starting point is 00:07:58 has to be a safe language, and it also has to, as a systems programming language, it also has to leave the developer in charge of when memory gets read and when it goes away. And this is actually really nice, because when I work in JavaScript, I actually feel really uncomfortable. If I'm going to build up some big table or some big kind of map, I'm always concerned that if I don't manage it very carefully, that I'm going to end up, you know, creating something that's going to bloat over time. Like the longer you have this page open or the longer you have this applet open or whatever, the more memory it's going to consume, right? And so I end up sort of going over my code with a fine-tooth comb to look very carefully at what I'm retaining and what I'm pointing to, right? And in the end, that kind of care looks a lot like the same kind
Starting point is 00:08:52 of care that a C++ programmer has to invest in making sure that their pointers are valid, right? And so it's actually just two sides of the same coin, it's just that in the javascript case or in the in the gc case the failure mode is is relatively benign um i'm not sure that a lot of javascript programmers are are as concerned like maybe they should be but yeah well the evidence suggests that many people are not too concerned about this right so so neither of those compromises acceptable for rust and so rust has to actually, make sure that your programs, make sure that your pointers are always legit, while B, still leaving you in control of when memory gets freed. And it takes a sort of, there's a two-level approach. The first step is to make sure that every value in the system
Starting point is 00:09:47 has a very clear and obvious owner, right? We are accustomed to thinking about the heap in a language like Java or in a language like Haskell or in a language like JavaScript. We're accustomed to thinking the heap is just a big graph of objects, right? And you could build any kind of structure in that graph you want. But what that means is that it's not clear to the system at all. And there was no source level indication of how long different parts of that graph are supposed to live. And so Rust says that every value must have a clear owner. And basically all of these owners are rooted in local variables, right?
Starting point is 00:10:30 Variables that are local to a call. Rust has global variables, but their use is very restricted. And they're actually kind of a pain. So for most intents and purposes, you can say that Rust doesn't use global variables. And that means that the ownership of every object in a Rust system is rooted somehow, somewhere in some local variable. Now, that means that if you've got a local variable, it's a type string. Well, it's pretty obvious that the local variable owns that string, right? But what if you have a hash table entry? Well, you have a big table of strings, right?
Starting point is 00:11:15 Well, then you'd say that the hash table owns the keys and values that are stored in it, and then something else owns the hash table. But the keys and values are uniquely owned by the hash table, and the hash table is uniquely owned by something else. You can go up and up from the owned value back to its owner, and eventually you find yourself rooted in some local variable. And what this means is that since every value... Okay, so maybe I didn't say this, but every value also has a unique single owner, right? Which is a little weird. Just one.
Starting point is 00:11:44 Just one, right? There's always a very clear single owner. And there are ways to break out of that. There's actually an explicit reference counted pointer type. And that's something where obviously if you have many of these reference counted pointers, it's only when all of them go away that the object gets freed, that the thing they're pointing to gets freed. But that's something that you have to ask for explicitly. And so the standard, the usual behavior in Rust is that every value has a unique owner. And when that owner goes away, when that variable goes out of scope,
Starting point is 00:12:17 or when that element is removed from the hash table, or when the vector has that element deleted from it, when the owner goes away, the value is freed at that time. And so that's how you get control over the use of storage. It's like your variables go out of scope at the end of the block if you're not returning them. It's when you delete an element or when you remove an element from a data structure
Starting point is 00:12:41 that its contents get freed. Freeing memory always happens in response to some specific thing that you did in your program, right? And so that's the way Rust puts the developer in control of the lifetime of the values. Now, that model has the merit of simplicity, but it's really limited and you can't actually write real programs that way. And so there are two ways that Rust sort of relaxes the constraints on this. And the first way is that you can move values from one owner to another. For example, a vector.
Starting point is 00:13:21 So we've got a vector of strings, right? The vectors pop method, it takes the last element, it removes it from the vector, and it returns it to you. Okay? When it returns that value, that's not simply, you know, it's not just like returning a pointer to it. It's actually moving ownership of the value from the vector. The vector no longer owns it, and it transfers ownership to the caller of pop. So whoever called pop, now they are the owner of that element. And so if you imagine you're
Starting point is 00:13:51 popping off an integer, then this is kind of silly to talk about. But if you imagine you're talking about a vector of strings, and the strings would be really big, then it makes sense to say, oh, now I own this string, and I'm responsible for deciding how long it lives um or you know a vector you could have vector of anything in a vector of gigantic hash tables right so in in all the situations um moving ownership is something that that makes sense and and it preserves this property of unique ownership uh that the the value used to be owned by the vector and now it is owned by the caller of pop um but but it had a single owner at every moment. I think, uh, I think you picked pop for a reason. Cause, um, what, what if, uh,
Starting point is 00:14:33 so I'm imagining pop, you have some sort of memory where the array is and now we're taking off the last element, but, but what about something in the middle? Okay. Yeah. So it turns out that Rust does not have any primitives, which would allow you to move a value out of the middle of a vector. And so this leads us to the third kind of access, right? I said there are various ways that we relax it. The first is moves, right? That you can move ownership of value from one thing to another. And whenever we build up complicated values, we're basically building them up one move at a time. The second way that we relax the rules is that we let you borrow values, right? There's a way to leave the owner of a value undisturbed. The owner doesn't change, but you get to temporarily grant
Starting point is 00:15:23 access to that value to somebody else by borrowing a reference to the value and so when you refer to an element in the middle of a vector right what you get when you subscript a vector the subscripting operator returns a reference to that element right it borrows a reference to that element now Now, references are, they are pointers, but they are constrained. They are constrained by their type to not outlast the thing that they are borrowed from. So if you have a vector, and then you, you know, you refer to some element of that vector, the type of that reference that you get to that element marks it as something that must not outlast the vector. So if the vector goes out of scope, the reference that you borrowed to its
Starting point is 00:16:15 element has to have gone out of scope first. Or if the vector gets moved away, you can't move that vector to some other owner until the reference that you borrowed to its element has gone away. So references are statically constrained to only live for a certain restricted part of the program. And that's how Rust makes sure that references are always still pointing to something that's still there. So you've got owning things and owning references, owning pointers can last forever. You can move them around. But references are constrained to live within a certain lifetime.
Starting point is 00:17:01 They can't outlast the thing that they point to. Actually, maybe it would make sense to talk about an example. So just for movement, there's this example in your book where you went through... So you had a Python example with an array of strings, and then you had a C++ example. Could you describe that for us? Yeah, sure. So what this part of the book is after, it's talking about this ownership thing. And one of the things that I wanted people to notice was that even the very simple idea of assignment, right, assigning one variable to another variable, is actually something where the meaning of that varies a lot from one language to
Starting point is 00:17:47 another. In Python, all of your variables are reference counted. So the example that I use, I say, well, let's say that we have a list of strings. Well, okay, the list has three elements, and it's got this little array of three elements that's allocated in the heap. And then each one of those elements points to a string, and the string has this text that's allocated in the heap. And then each one of those elements points to a string and the string has this text that's allocated in the heap. And the list has a reference count on it that says how many things are pointing to it. And so when you first create this list, the reference count is going to be one, right? And then the reference counts on all the strings that it points to are going to be one because they're each pointed to by the vector's elements or by the list's elements.
Starting point is 00:18:27 Now, if you assign that list to another variable, the only thing that happens in memory is that the reference count, the pointer gets assigned to the new variable, and then the reference count on the list gets bumped up. So now it says, okay, I've got two pointers to me. And when you assign, if you assign that list to a third variable, then the reference count will jump up to three. And what that means, the way Python has done this, it means that assigning from one variable to
Starting point is 00:18:57 another is very efficient, right? You're just copying a pointer over and incrementing a reference count. But it does mean that deciding when to free a value is kind of complicated, right? You can have pointers into this structure from anywhere in the program, and any one of them will actually keep the structure alive. And although Python does use reference counting for most of its management, it does have to fall back on a garbage collector in order to decide when to get rid of things in the end, because there are certain structures that reference counting doesn't handle correctly. So you need a garbage collector to handle this willy-nilly reference pointing to the same...
Starting point is 00:19:32 In general. In general. In general. Not in this particular example, but in general. You're going to have to look over the entire program's memory to decide whether anything's pointing to this. And so that means you've got to have a garbage collector. So if you write the equivalent program now in C++,
Starting point is 00:19:49 the story is very different, right? Let's suppose you have a standard vector of standard strings, okay? And you create it, and it's got three elements or whatever. And at first, this looks exactly like the Python situation. You've got a heap-alloc allocated array that's got three elements, and each one of those elements points to a string, and each string has a buffer in memory, heap allocated buffer in memory. Now, in C++, when you assign that vector to another vector,
Starting point is 00:20:20 the rules of C++ say that assigning a vector makes a fresh copy of the vector. And making a fresh copy of the vector means making a fresh copy of each of its elements. And then C++ says that making a fresh copy of a string, these days it says making a fresh copy of a string makes a fresh heap copy of that string. So if you take that list of three strings and you assign it to two other variables, you will have copied the entire thing over twice. And so you will end up with three different vectors and nine different strings, right? And so this is kind of surprising, right? Because you'd think that, I mean, assignment is such a primitive operation and it's the same for like passing values to functions, you know, or, you know, building data structures.
Starting point is 00:21:05 These are all, you know, this fundamental assignment-like operation. And in Python, it's really cheap to assign, but then you have to have a complicated thing to track when to get rid of things. And in C++, assignment can consume arbitrary amounts of time and memory, right? Now, the benefit of C++ is that when one of those variables goes out of scope, it always clears away, it always frees everything that it owns, right? And so the copying is expensive, and the lifetimes are simple, right? And there's observable behavior differences here, right?
Starting point is 00:21:42 Because in Python, I can make a change to my copy and the original one is also changed. Yes, exactly. The sharing is evident. The sharing is visible to the program. And it's actually this implicit copying behavior in C++ is sort of surprising to programmers a lot because one, it's not what most other languages do. Most languages behave like Python.
Starting point is 00:22:03 Java behaves the same way. JavaScript is the same way. And so C++, it's sort of a foot gun in C++ that you may end up inadvertently making expensive copies of something that you hadn't expected to be making a copy of at all. It turns out that at one point point somebody measured the allocator activity in the Chrome, in Google's Chrome browser, and it turned out that half of its calls to malloc were from standard string. Or it was caused by copying standard strings around. It was responsible for half the malloc traffic. And I'm not saying that that's uh it's not memory consumption but it's just the number of calls to malloc so this ends up and that certainly wasn't really necessary it wasn't what they had intended it's just the way that things ended up
Starting point is 00:22:56 um as a consequence of the of the the stock behavior of c++ so you say that this c++ copy is uh unintuitive but uh i found the rust behavior unintuitive okay well so the rust behavior sort of navigates a middle path between these two things it doesn't want to use a garbage collector doesn't want to require that you that you do something uh the garbage collectors are sort of unpredictable in practice people love them when they work and then when they don't work, it's bewildering. So Rust wants to leave the programmer in control of the performance of their program, and so it doesn't use a garbage collector. And it doesn't want to do the copies that C++ does.
Starting point is 00:23:43 So what Rust does is when you assign a vector of strings, you can have exactly the same type. It looks exactly like the C++ in memory. But when you assign that vector to a new variable, it will move the value. That is, when you say, you know, S equals T, you're moving the value of T into S, right? T becomes uninitialized. It no longer has a value. As far as the compiler is concerned, that is now an uninitialized variable.
Starting point is 00:24:12 And S now has taken over the value that used to be in T. It's taken ownership. And again, it's preserving this single owner property. And it's actually sort of, like I say, it's sort of the middle way because ownership is still clear, right? Now S owns the value. If S goes out of scope and you haven't moved the thing someplace else, that's when you free it. That's when you free the value. But at the same time, assignment is cheap. All you did is you copied over the header of the value. In the case of a vector, right, you've got these three words that say, here's the pointer to the heap allocated buffer,
Starting point is 00:24:49 and then here's the capacity, the amount of actual space it's got available, and here's the actual length, the number of elements stored in it presently. And when you do a move of a vector, it's only those three words that get moved over. The memory and the heap, the elements just sit where they are, right?
Starting point is 00:25:03 So the assignment is cheap. Ownership remains clear. So the assignment is cheap, ownership remains clear, and the only downside is that you can't use the source of the assignment anymore. It's had its value moved out, and it's now uninitialized. And so, yeah, I guess that is counterintuitive. These are called linear types because the idea is that you don't have this sort of forking of, you know, where a value suddenly splits into two values or a value suddenly has two owners. Every value is sort of makes a linear flow through, through time. It's actually affine types, but that's a bit of, that's sort of a type theory pun that's we probably shouldn't talk about.
Starting point is 00:25:50 It's a pun. Sorry. No, no, you shouldn't talk about it now. So here's the idea. So, so you have these linear types and these linear types were, were invented as a simplification of, or as a restriction of, of, of logic, right? It originated as people were talking about logics and, uh, you know, where you, okay, if you know that a implies B, you know, B implies C, then you can prove that a implies C. Right. And so, uh, people just, I don't quite know what the motivation was, but somebody invented something called linear logic, which, uh, where you only get to use a fact once, If you know that A implies B, and then you use that fact in a proof,
Starting point is 00:26:29 you can't use it again. And it's like, that doesn't correspond to anything in logic at all, right? Because if something's true, then it's true. But the nice thing is that it does correspond to values in memory, right? That is, if you have modified, if you have a big array and you modify it, you store a value in some element of it,
Starting point is 00:26:51 the prior value of the array is no longer available. You can't use it anymore. And so if you want your logic to serve as like a backing for a type system, then that's exactly the property that you want, right? You only get to use this value once because you've side-effected it and now the old value of it is gone.
Starting point is 00:27:10 You do a destructive update to something, well, something got destroyed. And that's what the linear types is. And so then somebody else, and so linear types were, like I say, they were a restricted version of the logic and they also required that every value will be used once. Oh, I see. You had to use it.
Starting point is 00:27:32 Right? Yeah, you have to use it exactly once, and that's why it's linear, right? If nothing collapses, nothing gets doubled, right? And so, you know, if you didn't want to use it, you could always just pass it to a function like drop. But the idea was that, again, this is going to be used to model values like arrays that needed to be updated destructively. And if they were big, heavy, expensive things to allocate, you wanted to make sure that when people freed them, they did so knowingly. And so the idea was that you would require them to drop things explicitly, too. And then it turns out that people don't really care about that as long as a value doesn't get used twice right it's
Starting point is 00:28:11 clear why you can't use you know why a destructive update means that you can't get at the old value anymore um but it's you know it's actually fine if you just let the system go ahead and clean up things if you don't happen to use it at all. And so when you relax linear types and allow people to just drop values on the floor, well, it's sort of like linear but better. Well, okay, so in linear algebra, you can have a linear transformation, which is just like, say, if you're doing it in two space, it's a two-by-two vector. But then you can't do translations in linear transformations. You can't actually move the origin to a different spot. You can't move things around. So if you do want a transformation that can move things around,
Starting point is 00:28:56 you need to use what's called an affine transform. This is a 2-by-3 vector or a 3-by-3 vector in homogenous coordinates. And so an affine transformation in linear algebra is a generalization, a relaxation of a linear transformation. And so an affine type system is a relaxation or a slight generalization of a linear type system. And so it's this kind of stupid in-joke, right? But that's where... And so, strictly speaking, Rust has an affine type system in that if you don't use a value when you get to the end of the block, if you didn't use it, it gets freed.
Starting point is 00:29:36 I shouldn't have asked you a joke where the punchline involves knowledge of linear algebra. That one's pretty weedy so to bring it back uh so um i'm not saying it's bad that assignment's different but i think that you know there's a there's a information theoretic perspective in which what you find surprising about a language is probably the most important thing and certainly this is a surprising thing thing that Rust has changed what assignment means. Yes. Well, okay. So, in my defense, in Rust's defense, first of all, I've pointed out that Python and C++ already disagreed drastically on what it meant. And so, if Python and C++ are
Starting point is 00:30:21 allowed to do drastically different things with assignment, then I think it's perfectly legit for Rust to choose yet a third way. But yeah, I mean, I think you're right that what's surprising is an indication of sort of where the most information is being conveyed. I mean, the thing is, when you think about it, C++ is a mature language. You've got a lot of people putting a lot of energy into pushing that idea as far as it can go and you know how local maxima are right you're not going to get significant advances um from something as polished as c++ something as mature as c++ um you're not going to make significant advances along one axis without giving up something else along another axis.
Starting point is 00:31:11 And so this is just Rust's, one of the bets that Rust makes, is that actually this is something that people can learn to use and something that people can be productive in. And learning to program wasn't really easy to begin with, right? When you were learning to program, you learned to internalize a whole lot of really strange stuff. And why does it always have to be the same strange stuff? Why can't we have some new strange stuff? And basically, in my experience, and I've talked to some friends of mine about this, it really is something that you can internalize just as well as the other things.
Starting point is 00:31:51 And it really does correspond to the costs of the computation you're doing in sort of a better fit than the c++ world c++ is trying to give you the illusion of a value-oriented world where assignment just means that you've got a copy um but that's not actually the world that we live in we live in a world where we care about memory where we care about um time spent uh you know copying things and so the mismatch with reality is actually on C++'s side here. And what Rust is doing is it's simply taking the real costs that you are always paying and making them show up in the type system. That makes sense. So back to your example, when I do this assignment, and so now it's a move. So my previous value is now uninitialized. So aren't I now reintroducing the C++ problem
Starting point is 00:32:48 of being able to access something that's uninitialized or that's been freed? Well, so one of the things, Rust forbids you to use an uninitialized variable, right? I mean, you can declare a variable without setting its value. You can say, you know, vector. I want v to be a vector of strings, right?
Starting point is 00:33:08 And you don't actually have to provide an initial value at that point when you declare it. But Rust does require that every use, every read of that variable must be you must be unable to reach that read from the declaration without first going
Starting point is 00:33:24 through an assignment to it. That is, every path from the declaration without first going through an assignment to it, right? That is every path from the declaration of the variable to a use of the variable has to be preceded by something that initializes it. And so Rust is already doing a certain amount of sort of flow-sensitive detection of, you know, hey, it's tracking our, you know, where in this function, at which points is this value initialized, at which point is this variable non-initialized. And when you do a move, that variable becomes uninitialized at that point. And so you get a static error if you try to use it. You get a compile time error if you try to use a variable
Starting point is 00:33:57 whose value you've moved from. And that's really something that I haven't seen. The table stakes on compilers have been lifted up here, I think, to a higher level than I'm used to. Yeah, I mean, what's going on is that Rust is really trying to say, we want to be a sound language, right? People talk about memory management as the big thing, but really what's going on is that Rust wants to be a sound language, right? People talk about memory management as the big thing, but really what's going on is that Rust wants to be a sound language where if a variable has a type or if an
Starting point is 00:34:33 expression has a type, that every value which could ever appear there as the value of that expression or as the value of that variable really has that type. And that sounds like a really modest promise. It's almost tautological, right? Except C++ doesn't make that promise, right? And so the tracking of what's initialized and what's not initialized is really just in service of making sure that the language is sound, right? Obviously, an uninitialized variable has a type, but it doesn't hold a value that's been properly initialized at that type. And the memory stuff is really just, again,
Starting point is 00:35:13 it's just a way of serving the principle of soundness. If you want dereferencing a pointer to have the type that the language says it does, you've got to make sure that that pointer is valid. And the soundness is really what it's all aimed at. So I'm not sure about C++, but like Java, for instance, has nulls and they just say like,
Starting point is 00:35:35 that's just part of the set of possible types that this can be. So this is an object and one of the possible object values is null. How does Rust handle that? Well, so Rust actually takes a page from Haskell and ML and I think another number of other languages like this. And it says that pointers can't be null.
Starting point is 00:36:01 Rust safe pointers are never null. And if you want to have a nullable pointer, you just have to say it explicitly. There's an option type, which is like, I think it's maybe in Haskell. And I think it's also option in ML. And what that means is that you don't have, when you've got a nullable pointer, the type of that value is not pointer. It is option of some pointer type. And so you actually have to match on the option, or you have to do something to check whether the pointer is actually present before you can use it. And so this is kind of a funny thing, because unlike the moves and the borrowing and the references, that stuff is really heavy, complicated, strange, new type theory. Option, making pointers non-nullable and then requiring people to
Starting point is 00:36:56 use option when they do want a nullable pointer, that's really sort of straightforward. There's just, you're not allowed, there's no way to create a null pointer. And when you do want to have something that may or may not be there, you have to wrap it in an option. And it's really easy to understand. But it makes a huge difference in the code. One of the things that I find now when I work in C++ is, you know, I still work on Firefox, and Firefox has this huge C++ code base, is that it's actually pretty scary how often there are implicitly nullable pointers that really are sometimes null under certain circumstances. And there's no indication of this. If you want to find out whether a pointer is safe to dereference in C++,
Starting point is 00:37:47 you've got to hunt around. If somebody left you a comment saying this pointer is always non-null, then that's really awesome. But for the most part, you have to hunt around, you have to try to guess, you have to see if other people are checking for it to be null elsewhere. But that's dicey, right? Because maybe they already know. Maybe they checked previously.
Starting point is 00:38:08 It's not a sure thing. And so C++ code is really riddled with all of these invisible option values everywhere, and you don't know which ones they are. And it really makes the language a lot harder to work in. Whereas when you're in Rust, if there isn't an option in that thing that you know, it's there. And it's much, much simpler. And when you do put an option in Rust, in Rust code, then suddenly, every time you use it, you have to you have to check it. And it's, it's a little bit of a pain.
Starting point is 00:38:44 But what it does is it pushes you to say, well, wait, do I really need this to be optional? Maybe I can, instead of using an option here, maybe I can just simplify my type and guarantee that the value is always there. And now what you've done when you do that is you've taken your type, and it used to have two variants, right? It could exist in either of two states. Maybe the pointer's there. Maybe the pointer's not there. And when you say, wait a second, no, I'm not going to do it. I'm just going to guarantee that the pointer's always there. Now you've halved the state space of your
Starting point is 00:39:11 type. And that is really a big help. And so I think what's going on is that nullable pointers, implicitly nullable pointers like you've got in Java and like you've got in C++, invite programmers to create types with many variations. They could be in many, many different states which need to be handled distinctly in a way that is invisible. And that the reason that we have null pointer crashes as a problem or null pointer exceptions in Java, the reason that we have, you know, null pointers as a null pointer crashes as a problem or no culture exceptions in Java. The reason that we have that kind of thing showing up so often is because it confuses people. And like, you know, the JavaScript has the same problem.
Starting point is 00:39:54 You get, you know, defined is not a function, right? Or you get you get these things showing up in consoles across the web. And the, the, I forget the, the man's name who came up with the null concept and said it was like his biggest mistake ever. Tony Hoare is his name. Yeah.
Starting point is 00:40:14 It's actually the same last name as Graydon Hoare, who's the inventor of, is the inventor of Rust or the original designer of Rust. Yeah. Tony Hoare called it his, his billion dollar mistake, introducing null pointer into ALGOL. And the funny thing was that basically he said it wasn't something that he thought through very carefully. It was just sort of like, you know, as a draft
Starting point is 00:40:36 unit, yeah, well, I mean, I guess you could just make zero be a legitimate value and then you could have a check for it. And sure, yeah, it seems fine. Yeah, we'll throw it in. And then like, you know, 30 years of people losing track. legitimate value and then you could have a check for it. And sure. Yeah. It seems fine. Yeah. We'll throw it in. 30 years of people losing track. Oh, it's, it's a plague for sure. Um, once you don't have it, I think it's, well, I mean, I, yeah, you still have options. We have to be explicit and I think it's such a great, I think it's such a great change to have it explicit rather than anything anywhere. Yeah. Yeah. Option option is such a boon. And it's like,
Starting point is 00:41:04 and like I say, it's super low tech. This is not complicated type theory. This is, this is just a, uh, an enum with two variants and that's it. And it's,
Starting point is 00:41:13 and it's such a help. So like in, in Java, in C sharp and in lots of other things, we have, uh, values that, that can't be uninitialized.
Starting point is 00:41:24 Um, like, like an int is, is not going to be null in, in Java. It's going to have like value of zero or something. Um, so how does that work in Rust? Do they move? Do they, are they defaulted? Okay. So, so, um, yeah, I mean, for, for the security, for Java to have the security properties that it wants to have, it has to make sure that your values are sure that your variables are always initialized. So it's got the same kinds of rules that Rust does.
Starting point is 00:41:50 So there are certain types. I think what you're getting at is copy types. There are certain types that are so simple that it's kind of ridiculous that moving them would leave the source uninitialized. Like if I've got, you know, an integer in a variable, by the time I have moved that value to another variable, I've already made a complete independent copy of that value, right? You're just moving 64 bits from one spot to another, and there's no reason to mark the source of that assignment as uninitialized because it's perfectly usable. Now, that's very different from a string, right?
Starting point is 00:42:30 If I have a string, a string has a pointer to a heap allocated buffer, and when I move it from one variable to another, treating both of those as live values is actually dangerous because you've got two pointers to the same value, and it's not clear when that heap allocated buffer should be freed anymore. So it's only types where the act of doing a move effectively gives you a working copy, a legitimate copy of the value. And so that would be types like int or floating point values or care or Booleans.
Starting point is 00:43:03 And also if you just have a structure that only includes such types, right? If I've got a struct of two ints, right, then certainly assigning that could legitimately, you know, I've effectively made a copy. So what Rust does is it says that there's a special class of values called copy values. And when a type is copy, is the way we say it, when a type is copy, that means that assigning a variable of that type to another, assigning a value of that type
Starting point is 00:43:32 or passing it to a function does not move the value. It copies the value. And so integer types are copy. Like I say, all the primitive types are copy. And so if you pass an integer variable to a function, then that integer variable still has. And so if you pass an integer variable to a function, then that integer variable still has its value. If you assign that integer variable to some other variable, then it still keeps its value. And this is sort of an ease-of-use thing because it's just silly to make people explicitly copy
Starting point is 00:44:01 or clone values when they're trivial. Now, when you declare your own type, like I said, the example that I gave before was if you make a struct of two values, like say you've got a point that's got an x and a y coordinate, you may want that type to itself be copy. And you can ask Rusk to mark it as such. And as long as the value, as long as your type doesn't have anything, as long as your type consists only of members that are copy themselves, Rust will say,
Starting point is 00:44:31 yes, your value can be copy. But if your type contains anything that owns resources, like a string or a vector or a file descriptor or anything like that, then Rust refuses to make it copy. And so copy is really restricted to things where a bit-for-bit duplication is all you need. And if you need to do any kind of resource management, then you can't make it copy.
Starting point is 00:44:54 But you can implement the clone trait, and clone implements the clone method, which makes a deep copy of the object. And so, for example, if you wanted to get the C++ behavior of assignment, where you really did want three copies of your array with strings, then you could actually call clone, and then you could get the C++ behavior. But basically, you couldn't declare, you can't take something like that and make it a copy type.
Starting point is 00:45:25 Copy is only for simple things where a flat copy is all you need. And I think that doesn't seem that strange to me, like coming from Java or C Sharp or whatever, where everything is like a reference type except for these certain fixed size primitives, which act a little bit different. Yeah, it's very much the same idea. I mean, well, let's see. The trick is that with those languages, the reference types, like in Java and C Sharp, object types or vector types,
Starting point is 00:45:59 I guess they're arrays, they're called arrays. Object types and array types are reference types. And so when you can assign one of those, but now you've got two references to the same thing. And you've started to make it ambiguous who the owner of that value is. And then that's the kind of thing where you start needing to have a garbage collector for that. Yeah. Earlier, I guess right at the beginning of our interview you talked about uh references so if i if i want to if i have my my earlier example my vector of strings um and i want to like print it out um so i have a print function and it takes in a string and then it
Starting point is 00:46:41 whatever console writes it um getting it getting, it will become the owner of the string I pass into it and then it will print it out and then it will fall out of scope and then it will be free. So if I print my, if I print my vector, it ends up empty at the end. If I loop over and print it, is that, is that correct? Well, so this is, yeah, that, that if you do it the wrong way, then that's correct. So the, the, like I said, we started off with everything as a single owner. And then we said, well, that's a little bit too restrictive to make everything have a single owner.
Starting point is 00:47:14 Let's let you move things around. And then we said, okay, well, moving things around is helpful for building things and tearing things down. But a lot of times we want to temporarily grant access to something without changing its owner, and that's where references come in. So if you write, say, your printing function, and the type of that printing function, its argument, the type of value passed to it is simply string, well, then that's a move.
Starting point is 00:47:39 And when you call that function, you are moving ownership of the string into the printing function, and then either it's going to have to return it back to you, which is a pain, or it's just going to consume it, which is silly. And so what you can do instead is you can borrow a reference to it. And then if the argument type of that print function is reference to string, then what that means is that you borrow a reference to the string, and the ownership remains with whatever owns the string to begin with. And then the print function gets to temporarily
Starting point is 00:48:12 have access to the string. And go ahead and do whatever kind of formatting and IO that it wants to do. And then when it returns, that reference has gone out of scope, and the owner is now free again to do what it likes with it. But while a value is borrowed, there are two ways to borrow a value. You can borrow a shared reference to a value, which just lets you look at it. You can't modify it, but you can make as many shared references to something as you want. You can have 20 things all pointing to the same thing. Or you can borrow a mutable reference, which is exclusive. You can only have one mutable reference to something at a time.
Starting point is 00:48:53 And that grants the ability to modify the value. So, for example, if you wanted to call a function that stuck stuff onto the end of your string, you'd have to pass it to a mutable reference to the string. And then while that function is running, it has exclusive access to that string. Nobody else can even look at it. Not even the owner can look at the string, right? But when it returns, that mutable reference goes out of scope, and then the owner again has access to it. So, and again, these are ways to grant access to temporarily let, say, a printing function examine a string or to let a
Starting point is 00:49:28 formatting function add stuff on to the end of the string right um and then when that temporary when that borrow is done uh the owner gets control back and these are these are static guarantees. So if I understand, then at runtime, these are just pointers like it might as well be C, but to actually compile it, then we're doing static analysis and making sure that these rules hold. Right. What's going on, yeah, certainly at runtime,
Starting point is 00:50:00 these are just ordinary pointers. The runtime implementation is just exactly like what you'd find if you were passing something by reference in C++. But the only difference is that a reference type, the type of a reference value,
Starting point is 00:50:16 has a lifetime attached to it, which represents the section of the program over which this reference is valid. So, for example, if you have a string that's not a reference yet, if you have a local variable that's a string, it's local to some block in your program, right? The lifetime of that string is from the point of declaration
Starting point is 00:50:35 to the end of the block, or from the point of declaration to the point that it gets moved, the value gets moved someplace else, right? But until that point, the value is sitting right there, okay? When you borrow a reference to that string, the type of that reference says this type may only be used within the lifetime of that value until it gets moved or destroyed, or the things that it calls, right? And so if you ever try to assign a value of that reference type to, you know, something outside the lifetime of the underlying string, that's a compile time error.
Starting point is 00:51:12 That's a type error. When you call a function, the function actually has to take parameters, has to take their like type parameters, their compile time parameters that represent the lifetime of the thing that's being passed to it. And the function has to be okay with getting an object of a restricted lifetime. And so it's really kind of nice.
Starting point is 00:51:38 If a function is going to go and store a pointer to something in some big data structure somewhere, then in the type of that function, the reference that it accepts is going to say, by the way, I need a really big lifetime. And so if you try to pass to it a reference to some local variable, you get a compile time error because you're trying to call a function and pass it a reference. And the function says that it needs a reference of this really big, long lifetime, unconstrained lifetime,
Starting point is 00:52:07 but then that's not the lifetime of the value, the reference that you're passing it doesn't satisfy that. And so you get a compile time error if you try to pass a pointer to one of your locals to something that's going to try to retain it for longer than that. So in the simple case, so I take my print function,
Starting point is 00:52:30 and now it takes a reference of string into it, and it prints. So where's the lifetime parameter there? So a lot of times, there is a lifetime parameter there, but a lot of times you don't have to write it. For example, if you just have a print function, maybe that print function takes a single argument, you know, string of type, shared reference to string, okay?
Starting point is 00:52:52 And it doesn't return any value. That type signature is really simple. And you could write out the lifetime in that signature and name the lifetime that that reference has. But because functions like this are really common, Rust has sort of a shorthand. It'll look over the signature of the function, and if it's very obvious from looking at the signature of the function what the lifetimes involved need to be,
Starting point is 00:53:20 then it will actually go and stick them in for you. But the lifetimes really are there. They're just being elided. There is a type parameter, I guess, and that type parameter is the lifetime that this will run. And when it infers it, I'm assuming, it just infers it to be that the lifetime of the thing I pass in must be... I'm having trouble picturing how it infers it, I guess.
Starting point is 00:53:46 Okay. Well, so let me actually spell out what the real signature... What you write out in your code, what you actually put in your code is you say print, and you say open paren s colon ampersand string close, right? And that means that there's this function print, and it takes takes a parameter named S whose type is shared reference to string. And then it doesn't return any value. What that is shorthand for is print. And then in angle brackets, you say tick A, like a single quote A.
Starting point is 00:54:16 And that means the lifetime named A. And these are angle brackets. So this is a type level parameter. This is a compile time parameter. And then the argument type is actually ampersand tick A, lifetime A, string. So in other words, what this declaration means is for any lifetime A outside the call, I'm going to take a reference restricted to that lifetime to a string, and then I'm not going to do anything. I'm not going to return anything. And so when you write this function,
Starting point is 00:54:50 Rust will look at that and say, well, okay, tick A, this lifetime that you put in, it could be, you know, very tightly wrapped around the point of the call, right? Maybe this variable was created just before we did the call, and it's going to be destroyed just after you do the call. So you can't assume that this lifetime A is any longer than pretty much the call, the point of call on the caller. And so if you try to actually store that reference someplace that lives longer, that won't work. You'll get a compile time error. So the callee makes conservative assumptions about what the lifetime could be.
Starting point is 00:55:28 And if you try to do something more ambitious with that reference, then it will say, I'm sorry, your ambitions are not supported by the constraints that we have on this lifetime. I get it. So I think this clicked for me. So the default is any lifetime. And then what that has to mean is that it can't do anything with it outside. Like it has to, it can't store it anywhere because, um, conceivably after it's called, then, then that, uh, the thing passed in could fall out of scope and therefore be uninitialized. Yeah. It's not, it's not quite the default isn't quite any lifetime. The default is any lifetime
Starting point is 00:56:10 that encloses the call. Any lifetime that encloses the call. Right. If I'm taking, if I'm taking a reference as an argument, then clearly that argument must have been when I was past it. Okay. At the point of the call, that reference must be legit. And so therefore I can assume that it is legit for the duration of this call, but not any time before or after that. But yes, otherwise, the effect is just as you said. Because if I have... So a lot of the times, you don't even have to worry about this lifetime, because what I'm trying to come up with an example is when we're returning one argument, but the lifetime of the two parameters could vary, right? Is that right? Okay. So you're imagining, let's take a function that
Starting point is 00:56:49 takes references to two values and randomly returns a reference to one or the other. Well, that sounds even more complex. I was just, I was just thinking of if these two variables have two lifetimes and we return one, then we would need to specify that the returning one, which lifetime it had. Okay. You can do that, but actually that one works out okay. And I'm trying to see if I can actually explain why it's okay. So, so the, the, the lifetimes that show up in a function signature get used in two ways. One, they get used to check the body, the definition of the function itself. And two, they get used to check the call of the function. So it's clear that in this particular case where we're going to take, for the body of the function, for the definition of the function,
Starting point is 00:57:38 we're going to take two references and then one of them is going to, we're going to return a reference to one of them. Well, it's pretty clear that the function can't, it's not storing it anywhere. It's just handing it back. And so that's legit. And so you're not going to get any problems with the definition of that function. Where things get interesting is in the caller. The caller is going to pass in two references to things with different lifetimes, and then it's going to get back a reference. Those two references that get passed in are initially, from the caller's side, they could be anything, right? This call doesn't impose, basically, it's not going to restrict that. And so it's not going to restrict those two lifetimes very much. And what it will do is it will actually say, well, okay, you passed in.
Starting point is 00:58:35 If you treat them both as having the same lifetime, it will say, well, okay, which lifetime works for both of them? The largest lifetime that works for both of them is the smaller of the lifetimes of the two things that you borrowed references to. So it picks the most restrictive. It picks the largest one that is still safe. And so what you will get is a reference to... The reference that it returns will be usable within the smaller of the two lifetimes. You have two objects with different lifetimes. You borrow references to each of them them and then you call this function the reference that you get back
Starting point is 00:59:08 will be restricted to lie within the lifetime of this of the the the object that has the shorter lifetime so with your previous example of like returning randomly one then it still can just pick the most the the most restrictive lifetime. Yeah. Yeah. So, yeah, somebody told me before, like, uh, it's good to have a technical podcast, but you shouldn't, you shouldn't talk about code. Well, I broken that rule. We've totally broken that rule. I really, I'm not sure. I hope this is going to be okay without like a whiteboard or something. I think it's great. Uh, I think, I mean, to me, without like a whiteboard or something i think it's great uh i think i mean it's me so
Starting point is 00:59:46 like it's great i hear lots of things about rust and people complain about the borrower checker and whatever right but if you don't have examples um like you don't really understand it so i mean i think the things we've talked about are are very small pieces of code but they they help to highlight um you know what what it actually means when people say they're fighting with the borrow checker, right? So, so just so you know I don't really fight with the borrow checker much anymore. I can kind of anticipate how it's going to behave and how it's going to think. And I have co-workers who don't, who don't really fight with it. A lot of what's going on is not so much learning to deal with the borrow checker, but rather learning to structure the way that you use your values in trimmed back to this very sort of restricted model that Rust pushes you into. And of course, when you're being told that you can't do something that you're pretty sure is
Starting point is 01:00:59 fine, you're going to chafe against that. But it turns out that really, almost everything that you want to do, you can fit into the model pretty well, you know, with exceptions, and then there are workarounds for it. I think I mentioned in the first podcast, I had lunch with a friend and they said, you know, it looks to me like Rust doesn't let me create cycles. And I said, well, yeah, cycles have ambiguous ownership. And he says, you know, as a programmer, I need cycles. I use them all the time. What he's saying is, is, is legit. But it turns out that you can actually do fine. There are, there are ways to, there are other approaches that you can take. And the payoff that you get is a static judgment about the viability and correctness of your program.
Starting point is 01:01:48 And that's really nothing to shake a stick at. It's really something that I really miss when I have to go back to C++. So it's a challenge, and it's a worthwhile challenge to do. So I don't feel like, uh, the bar checker is really something you have to fight with. The bar checker is simply, it's the discipline that you're learning. And this discipline is a valuable way to structure programs. The lifetimes portion at first glance, it's seems a little complicated, but then when I think of the idea that, that if I call something that's going to store, uh, what I pass into it globally,
Starting point is 01:02:23 like it can't hide that from me. Right. It's like, cause I think a lot more time gets spent like maintaining code or debugging issues. And to have this explicit information right there in the signature, you don't even have to, you know, dig into what it does. It's incredibly, it's incredibly valuable. I mean, like right now at work, I'm, I'm dealing with something where we have workers
Starting point is 01:02:44 that send messages to each other and I need to have some of those messages be delayed. For example, something's being debugged. It better not be delivering its messages while you're sitting at a breakpoint, right? And that means that I'm changing when these messages get delivered. And it turns out that it's very possible to change it so that the messages get delivered after the thing they're delivering to is gone, right? And so here's something where it absolutely would be a type error in Rust, or at least it would be something that the communications channel would recognize and cope with properly.
Starting point is 01:03:17 But in C++, not only do I get a crash, which is not unfamiliar. I've done debug crashes before. But it's like I have to hunt it down. In Rust, it would have just told me. So it's kind of a pain. So I think you've made a case that there's a style to Rust. And as a C++ developer, if you can adopt it, there's big payoffs. Right.
Starting point is 01:03:44 But as a as a developer who's never too far from a garbage collector you know i'm used to kind of building more i'm used to not worrying about references and kind of having things point to things all the time right um so is there a benefit to these constraints for somebody from a gc world yeah it's sort of it's sort of surprising. The restrictions on borrowing actually have valuable for even simple single-threaded code. One thing that's kind of bugged that you have cropping up
Starting point is 01:04:13 in complicated systems from time to time is where you have a function that is, say you have a function that's traversing some data structure, or it's operating on the elements of some data structure, and for each thing that it does, it's going to call out to somebody, right? You will occasionally come across these bugs where you're calling out to somebody and then they go do something else. And then they invoke an operation that goes back and modifies the very data structure that you are traversing. Okay. And so the way the stack looks is you've got this iteration in one of the callers,
Starting point is 01:04:48 right? It's iterating this data structure and then one of these callees is modifying the data structure as it's being iterated over. Now, in general, it's really difficult to specify exactly what you want from situations like that. And in Java, there's something called the concurrent modification exception,
Starting point is 01:05:04 which you'll get if you try to do this. And in Java, there's something called the concurrent modification exception, which you'll get if you try to do this. And in Rust, those bugs simply can't occur. They are compile time problems that get reported before your program ever runs. And so Rust is actually pushing you towards structures that always make it clear when modifications and things could ever happen. And that gives you a really nice sense of confidence. When you get a mutable pointer to something, you know that you have exclusive access to it. And that while you were working on it, nobody else anywhere in the system is going to come in and change it or modify it. And I think that that's actually a really valuable thing to have
Starting point is 01:05:47 in function signatures or in data structures. Some kind of promise that either you are looking at something that's being shared amongst a lot of people and it won't change, or you are going to modify this and you are the only person who can see your modifications. And it's a property that I miss when i go back to to gc languages uh and i so so i mean yes it is more flexible to be able to use to lean on the gc just create whatever kind of references you want um but i think i i do feel like i'm missing information that i wish i had and that rust
Starting point is 01:06:23 requires you to provide there's a there's a theme rust is making a lot of things explicit in the type system we talk about the option types like that one other other languages have seen that you know make it explicit if something could be can be not can be none or nothing yeah but also now we're taking it and we're saying all these lifetimes have to be explicit who has access to the thing who could be modifying this thing what sort of what mode of access of access is it in at this point in the program? That's also explicit. And that's just super valuable. Well, thanks for coming on for another interview, Jim. It's been great. Sure. Yeah, it was fun.

There aren't comments yet for this episode. Click on any sentence in the transcript to leave a comment.