I bet I can teach you Rust's borrow checker in less than 5 minutes!
August 6, 2023
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.
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:
What if the compiler handles all of this?
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"
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:
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
x as the owner for
To fix this problem, we "borrow" out the value of
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:
In this case, we pass
&x to the function, which simply means, pass the reference of
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
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.