Imagine you are out to dinner with some friends. You are all stereotypical Millennials so photographing your meal is expected. For every dish you order each of your friends borrows it, takes a photograph, and then returns it to you.
Well, that’s the plan anyway. Some of your friends are not the most reliable. Will they always return the right dish to you? Will they try to sneak a bite? Human friends are so unreliable. But if your friends were Rust functions you’d have nothing to worry about, thanks to Rust’s rules around borrowing and lifetimes.
Let’s model this dinner party in Rust and see if we can learn a thing or two about borrowing and lifetimes.
We’ll start off with the basics, a Dish that can be photographed.
We have a Dish struct which contains a name. There’s one function on Dish, photograph. You get your dishes from the hot new Italian/Ethiopian fusion restaurant and take their pictures.
Let’s introduce one of your friends. We’ll start off with your most honest and trustworthy companion. Being honest this friend wants to borrow a dish photograph it and return it. They don’t want you giving them more than one dish – that could get confusing! When converted to a Rust function, this friend looks like:
Our dinner-party now looks like:
Note that your friend is not explicitly returning the Dish at the end of their function. Thanks to Rust’s borrowing rules they don’t have to. When your friend’s scope ends, the borrow ends.
A new friend joins your table. This friend likes to play pranks, but they don’t always go as expected. Your Trickster Friend says, “Loan me a dish and I’ll give you a dish back. Trust me, I’m going to return to you the same dish you loaned me.” You have your doubts.
Try both options and you’ll see that your trickster friend can not prank you by giving you Pad Thai. The Pad Thai value is owned inside trickster_friend and is destroyed when trickster_friend ends. This is a bit easier to see if you inline the trickster_friend function into your main function
Our trickster friend shows us that if a function is going to receive and return a single borrowed reference it has to return the same reference it received. Even though our friend’s tricky plan failed we learned something from it!
A third, very impatient friend arrives to our party. They don’t have time for this “one plate at a time” nonsense; this friend wants you to loan them two dishes at once:
Just as with your honest friend, this works fine. We loan our two dishes to our impatient friend who takes their photos. When impatient_friend ends, so do the borrows.
But your trickster friend sees this exchange and gets an idea. They now know that they can’t return a plate of Pad Thai, but they figure that if they borrow 2 plates and only return one, they can keep that extra plate for themselves.
Foiled again! If you run that you’ll get error: missing lifetime specifier.
Remember what we first learned from our Trickster Friend? If a function receives and returns a single reference then it must be the same reference, otherwise you could return something that does not live long enough. That’s easy for the Rust compiler to check as there’s only one option. But this function receives two references and returns one. How does the compiler ensure that the reference you return points to a value that lives long enough?
In this particular code both our values – doro and bruschetta – live long enough. So it can be hard to see why the Rust compiler is confused. But what if we changed our code to:
If trickster_friend_again returns bruschetta then that code would be fine. But if it returns herring we’d try to photograph a dish that no longer existed. This is unsafe, so Rust won’t let us do it.
If the Rust compiler ever has any questions about what reference a function will return then that function must explicitly declare a lifetime. Our trickster friend could declare one of four lifetimes:
Option One:
Option Two:
Option Three:
Option Four:
Let’s see the differences between those options:
In this example we’ve declared one lifetime, given it to the reference passed to first_dish and our return value. Trickster friend now must return first_dish.
Let’s try the herring code again:
Using our current Trickster Friend this code will compile, because there is no way that mystery_meal will ever be herring.
Option Two is the inverse of Option One. The function must now return the second_dish. If we pass in herring as the first dish, our code still runs.
Option Three gives the same lifetime to both parameters so the function could return either of them. Our herring code will fail to compile regardless of parameter order because mystery_meal could be bound to a destroyed value.
In our dinner party Option Four is functionally the same as Option One. We’ve declared two lifetimes but we’re never using the second one making it irrelevant. I’m sure there are times when having two lifetimes on a function is useful, but I haven’t come across them. The book says you can have multiple lifetimes, but don’t explain why. I’d love to know more about this.
Is there more to lifetimes than this? Almost certainly. But I hope that this analogy helps you grasp the fundamentals of why Rust has lifetimes. If you want to learn more about fundamentals of Rust, maybe read one of my earlier posts, Rust via its Core Values. I also suggest the New Rustacean podcast, which has helped me understand quite a lot about Rust.