Picture of a person standing in grass in black and white

Hari Vishnu

I bet I can teach you Rust's borrow checker in less than 5 minutes!

August 6, 2023

I've been getting into Rust a lot recently and have fallen in love with it. There's something different about it that just feels right. I've been working with JavaScript and TypeScript for a year now and there wasn't anything in particular that I hated but I just wanted to try out Rust given the hype recently.

So what is so "difficult" about Rust??

Everywhere I look on the internet, the only thing that comes remotely close to "hard" about Rust is the borrow checker. Now, the reason I put "hard" in quotes is because I'm about to make borrow checker a child's play for you in some time.

Why do we need this "borrow checker" thing?

Now, before we get into what the borrow checker does, we need to understand the problem it's solving. I mean, there must be a reason for having such a "complex" mechanism right?

Let's take an example to explain things.

const stringVariable = "I'm hungry! 😞"

In the above simple example, we declare a constant variable stringVariable . Now when this code gets compiled to the really hard-to-understand-for-humans machine language, the computer needs to decide a place in the memory (RAM) to dedicate for this variable while the program runs. And, low and behold, we do not have infinite RAM! We can only store so many things in the RAM! If we use too much of our RAM, the program starts to take up too much space and slow down our computer!

Now, let's think about how we can go about fixing this problem... The most obvious solution here is to probably free the space in memory that was allocated to stringVariable if we know for sure that we're done with stringVariable and won't need it anymore!

Sounds like we solved the problem! Right?

Well... not really. The problem is, you see, it is very difficult to predict when we're done with a variable! Imagine it like this, you go through the program line-by-line when you come across stringVariable , how will you know when it was used last? You'll probably have to look ahead in the program for that. But let's not worry about that for now.

If we look at the problem, before deciding HOW the problem will be solved, let's first decide WHO will solve the problem. There are two options we have:

  • Hand it over to the compiler! It should be able to programmatically decide when we're done with variables.
  • Let the developer decide.
  • What if the compiler handles all of this?

    This is what most programming languages you're probably familiar with like Python and JavaScript handle the problem. The developer need not worry about when the variable is no longer needed. That's why, you see, you've probably never explicitly written in Python or JavaScript to destroy a variable and free its space.

    Now, this solution works. It has been used for decades and developers all around the world have used these programming languages to write critical software that solves real-life problems! But, there is an issue.

    The problem is the compiler. The component that looks for if the variable is needed in the software or not, is called the Garbage Collector.

    You can think of a garbage collector as a separate program running parallel to the actual code you wrote, to check when a variable can be safely freed. As you can probably infer from this, it makes things slow. Now, you need to run a new process along with your actual program. And when our code increases in complexity, this difference becomes obvious.

    Let the developer handle this!

    Rust handles things a bit differently. Every value in Rust can have only one "owner"

    fn main() { let x = 42; // x is the "owner" of 42 }

    Whenever the owner of a value goes "out-of-scope", for example when the function block ends, the variable is automatically freed.

    So, if every value has only one owner... what if we want to pass it as an argument to a function? I need to assign it a new owner right? Well, you're right. It would look something like this:

    fn empty_function(string_arg: String) {} fn main() { let x: String = String::from("value"); empty_function(x); println!("Hello, world! {}", x); }

    If you just keep in mind "every value can have only one owner", it's easy to see how this code errors. The variable x is passed as an argument to empty_function and assigned to the argument string_arg. Can't have both string_arg and x as the owner for "value".

    To fix this problem, we "borrow" out the value of x to string_arg . This means, instead of transferring the ownership to string_arg we simply borrow it out to string_arg so that we get its value back after empty_function. This would look something like this:

    fn empty_function(string_arg: &str) {} fn main() { let x: String = String::from("value"); empty_function(&x); println!("Hello, world! {}", x); }

    In this case, we pass &x to the function, which simply means, pass the reference of x to string_arg.

    Now, this reference is immutable by default, meaning we can't make changes to x from inside empty_function. If we want to make changes to x from inside empty_function, we need to pass a mutable reference by simply replacing &x with &mut x.

    So, how is it better?

    If we talk technically, the benefits of this approach are:

    Predictable Performance: Rust's memory management is deterministic and doesn't rely on the unpredictable behavior of a garbage collector. This leads to consistent and often faster performance.

    No Runtime Overhead: Rust doesn't need a garbage collector, so there's no extra runtime overhead associated with memory management.

    Fewer Bugs: The borrow checker catches many memory-related bugs at compile time, reducing the likelihood of such issues occurring in your program.

    Concurrent and Parallel Programming: Rust's ownership and borrowing system makes it easier to write safe concurrent and parallel code, as it prevents data races and other synchronization issues.

    Memory Efficiency: Rust's memory management model allows for precise control over memory usage, minimizing wasted memory and reducing the risk of memory leaks.

    If I remove all the technical jargon, it means that Rust lets many things happen at once safely, and uses just the right amount of memory.