I wanted a reference which covered a small piece of rust to help people get over one piece of the learning curve! What was that piece? How to call functions and return stuff. I know it sounds a little absurd to have to write a tutorial on how to call and return, but rust makes it a little more challenging than other languages in order to be as neato-keen as it is.
This tutorial assumes that you've probably already learned another programming language and can guess at the functionality of some of these snippets (or that you've looked at some other, more complete rust tutorials).
By default, variables are immutable in rust. So the following is an error (run it):
fn main() {
let x = 5;
x = 10; // ERROR: x is immutable!
println!("Can't touch this: Hammer Pants x {}", x);
}
Since we declared x
with a simple let x =
rust treats it as immutable. If you want to fix it, write it like this (run it):
fn main() {
let mut x = 5;
x = 10;
println!("Can't touch this: Hammer Pants x {}", x);
}
Which happily prints Can't touch this: Hammer Pants x 10
Something you'll do a lot is call other functions. When you call those functions, there are rules, and the rules in Rust are likely going to feel unfamiliar unless you've used C++11's unique_ptr
/shared_ptr
and move
.
Let's add a function foo
. When we pass a thing (struct/object/banana) into a function we actually give up ownership of that thing (run it):
fn main() {
let x = String::from("Hello");
foo(x);
println!("{} from the other side.", x); // ERROR: we gave away x to foo when we called it!
}
fn foo(bar: String) -> () {
println!("{} world!", bar);
}
What if we want to keep ownership? Well there's a few options!
We can make a full copy of x
, and pass ownership of that copy to foo
(run it):
fn main() {
let x = String::from("Hello");
foo(x.clone()); // Give away a full copy of x to foo
println!("{} from the other side.", x);
}
fn foo(bar: String) -> () {
println!("{} world!", bar); // I own this bar. I serve good beer.
}
This works, but is very expensive since foo
just prints bar out and throws away the copy.
"If only we could instead give foo access to a reference to x
," I hear you internally monologue. Well you're in luck! (run it)
fn main() {
let x = String::from("Hello");
foo(&x); // Give away an immutable reference of x to foo
println!("{} from the other side.", x);
}
fn foo(bar: &String) -> () {
println!("{} world!", bar);
}
"Neat! So a reference is just like a pointer, right?" you leap in, interrupting my super-sweet tutorial. Okay then hot shot, can you make the string "Break on through"
in this function come from foo
? (run it)
fn main() {
let mut x = String::new();
x.push_str("Break on through");
println!("{} to the other side.", x);
}
"Totally! I'll just pass a reference" you confidently say (run it):
fn main() {
let mut x = String::new();
foo(&x);
println!("{} to the other side.", x);
}
fn foo(bar: &String) -> () {
bar.push_str("Break on through"); // ERROR: x is immutable!
}
"Wait, no, that's not right... something like this?" (run it)
fn main() {
let mut x = String::new();
foo(x);
println!("{} to the other side.", x); // ERROR: we gave away x!
}
fn foo(mut bar: String) -> () {
bar.push_str("Break on through");
}
"Alright, so what if I just start shoving mut everywhere??!?!" (run it)
fn main() {
let mut x = String::new();
foo(&mut x);
println!("{} to the other side.", x);
}
fn foo(bar: &mut String) -> () {
bar.push_str("Break on through");
}
"AH HA! It works!" Good job personified reader! You did it! What happened was that you passed a mutable reference. There are some things you can and can't do with a mutable reference, but we'll talk about that a bit later.
Just for completeness's sake, here's the one other possibility that you could do (run it):
fn main() {
let x = String::new();
let y = foo(x); // Give away x, and put the result into y
println!("{} to the other side.", y);
}
fn foo(mut bar: String) -> String {
bar.push_str("Break on through");
bar
}
Some interesting notes about this one: see that x
was declared as immutable, but we still changed it! WHAT'S GOING ON?! Long story short: Rust only cares that when x
is in main
that we don't change it. When we gave it to foo
, we said foo
owns x
completely. Once foo
owns x
, foo
can do whatever it wants, even say x
is mutable.
Okay! Now you've seen everything you need to understand calling one function from another in Rust! Wasn't that easy? But wait... that last one... it has a "String" at the end of the declaration of foo
. Do we have the same sort of rules there?
I've been a bit coy about some terms. What does it mean when I say that foo
"owns" x
? I mean that foo
is responsible for deleting the memory and resources held by the bar
parameter. If we look at the first calling example again (run it):
fn main() {
let x = String::from("Hello");
foo(x);
println!("{} from the other side.", x); // ERROR: x has been deleted!
}
fn foo(bar: String) -> () {
println!("{} world!", bar);
}
See, when I say that foo
owns bar
, what I mean is that foo
is responsible for deleting the "Hello"
string when the function is finished! So the reason we can't access x
after calling foo
, is because when foo
finished, it deleted "Hello"
. So let's take a look at the last example from the previous section (run it):
fn main() {
let x = String::new();
let y = foo(x); // Give away x, and put the result into y
println!("{} to the other side.", y);
}
// This next line is important! It says "I will take ownership of a string, and then give ownership of a string"
fn foo(mut bar: String) -> String {
bar.push_str("Break on through");
bar
}
The function signature behaves a lot like the calling signature we saw before: it says "I'm going to give you ownership of a String when I'm done." And just like for calling, that means when you're done calling me you have to delete what I give you!
When we put that little &
into our code, what does that mean? Well, comparing to the example where we give ownership, we're instead saying "hey, we're lending you a reference. But the moment you return, that reference is invalid!" It's similar to the delete: when the function is done, everything should be cleaned up. So what happens when you want to return a reference? This is where we enter the real magic of Rust!
Let's start with an example. This one is going to be a bit weird, so we'll take it slow. This example works! Go ahead and puzzle it out (run it):
fn main() {
let x = String::from("Mysterious first letter!");
let ref_x = first_letter(&x);
println!("{}ystery solved!", ref_x);
}
// We'll take a reference to a str, and we're returning a reference to a str
fn first_letter(input: &str) -> &str {
&input[0..1] // We're taking a slice, with just the first letter
}
Now that we have a working example let's take it, break it, and figure out why it's broke (run it):
fn main() {
let x = String::from("Mysterious first letter!");
let ref_x = first_letter(&x);
take_ownership(x); // ERROR: ref_x still exists and is "borrowing" x
// We can't delete x safely!
println!("{}ystery solved!", ref_x);
}
fn first_letter(input: &str) -> &str {
&input[0..1]
}
fn take_ownership(of_this: String) -> () {
println!("I will destroy you: \"{}\"", of_this);
}
What happened? Why did it break? Rust looked at first_letter(&x)
and ref_x
and said "The thing that comes out of first_letter
references x
and is only valid as long as x
still exists. Therefore as long as ref_x
exists, we have to make sure x
still exists too." Then, we tried to give ownership of x
to take_ownership
, and Rust said: "Oh no! ref_x
still exists and after I run this line, x
won't exist! This is dangerous! STOP EVERYTHING!" This is super awesome! Rust protects us from trying to use the "borrowed" reference to x after x has been deleted. In other languages, using ref_x
after x
had been deleted is called a "use after free" bug. The mechanism Rust uses to do this is called the borrow-checker. Its job is to make sure that you didn't violate any rules when making and using references.
Okay, now that you've seen it working, let's see it doing something a little trickier (run it):
fn main() {
let x = String::from("What is love?");
let y = String::from("Baby don't hurt me,");
let ref_y = print_first_return_second(&x, &y);
print!("{}", y);
print!("{}", ref_y);
println!(" no more.");
}
// ERROR: I can't figure out if the returned value is a reference to "print_me" or "return_me"!
fn print_first_return_second(print_me: &str, return_me: &str) -> &str {
println!("{}", print_me);
&return_me[4..]
}
It doesn't compile! Rust says "hey, I can't figure out from your function declaration what you're gonna return. Please help!" The real compiler message is actually super helpful: "this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from print_me
or return_me
". Let's fix it! (run it)
fn main() {
let x = String::from("What is love?");
let y = String::from("Baby don't hurt me,");
let ref_y = print_first_return_second(&x, &y);
print!("{}", y);
print!("{}", ref_y);
println!(" no more!");
}
fn print_first_return_second<'a, 'b>(print_me: &'a str, return_me: &'b str) -> &'b str {
println!("{}", print_me);
&return_me[4..]
}
SUPER-WEIRD SYNTAX TIME! This is what's called a "lifetime annotation". Let's dig a bit into what a lifetime annotation is:
Rust is keeping track of when things are going to be deleted, and making sure we delete everything when it makes sense. The error from before was rust saying "I need to make sure you don't delete the thing ref_y
references, but I don't know when the thing ref_y
references will be deleted!" So the lifetime annotation is us putting a name to the time of deletion:
'a
is "the time whenprint_me
is deleted"'b
is "the time whenreturn_me
is deleted"
And then by putting the 'b
in the return reference we're saying: "what we return is safe until the time return_me
is deleted". So in the end, ref_y
must be deleted before or at the same time as y
.
We can see that Rust enforces this correctly, because it lets us give away x
(run it):
fn main() {
let x = String::from("What is love?");
let y = String::from("Baby don't hurt me,");
let ref_y = print_first_return_second(&x, &y);
print!("{}", y);
print!("{}", ref_y);
take_ownership(x); // This is okay, no one is borrowing x
}
fn print_first_return_second<'a, 'b>(print_me: &'a str, return_me: &'b str) -> &'b str {
println!("{}", print_me);
&return_me[4..]
}
fn take_ownership(of_this: String) -> () {
println!(" no more!");
// of_this will be deleted!
}
But in this example, rust yells at us for trying to give away y
(run it):
fn main() {
let x = String::from("What is love?");
let y = String::from("Baby don't hurt me,");
let ref_y = print_first_return_second(&x, &y);
print!("{}", y);
take_ownership(y); // ERROR: ref_y is borrowing y! We can't delete y!
print!("{}", ref_y);
}
fn print_first_return_second<'a, 'b>(print_me: &'a str, return_me: &'b str) -> &'b str {
println!("{}", print_me);
&return_me[4..]
}
fn take_ownership(of_this: String) -> () {
println!(" no more!");
// of_this will be deleted!
}
And one last error case, where we use the wrong lifetimes in print_first_return_second
(run it):
fn print_first_return_second<'a, 'b>(print_me: &'a str, return_me: &'b str) -> &'a str {
println!("{}", print_me);
&return_me[4..] // ERROR: we said that the returned reference would be to print_me, but this isn't print_me!
}
In the first example where we returned a reference, we didn't put any lifetime annotation, how did that work? Well dear reader, this is because rust tries to infer a lifetime. Here's the first example again, with the inferred lifetime annotations made explicit (run it):
fn main() {
let x = String::from("Mysterious first letter!");
let ref_x = first_letter(&x);
println!("{}ystery solved!", ref_x);
}
fn first_letter<'a>(input: &'a str) -> &'a str {
&input[0..1]
}
Since we only had 1 input, and we were returning a reference, rust just guesses that the reference and the input are one and the same.
Rust will infer the lifetime in one other important context: when you have an object with methods. Whenever you implement an object, member functions on that object will have a &self parameter. By default, rust will guess that the lifetime of the returned reference is the lifetime of the object. To make this obvious, I put the inferred lifetimes in this example (run it):
struct Neato {
burrito: String
}
impl Neato {
pub fn new(ingredients: &str) -> Neato {
Neato { burrito: String::from(ingredients) }
}
// I put explicit lifetimes here, but rust would have guessed this normally
pub fn print_and_unwrap_burrito<'a>(&'a self, print_me: &str) -> &'a str {
print!("{}", print_me);
&self.burrito
}
}
fn main() {
let x = Neato::new("cheese, beans, rice, and steak");
let y = x.print_and_unwrap_burrito("Neato burrito with ");
println!("{}", y);
}
This example will work whether or not the 'a
is in the code.
Alright so there's two more situations you should know about if you want to get all this shenanigans working.
The first is returning a reference to a string which is just part of the program, i.e. it doesn't need to get deleted at all! (run it)
fn main() {
println!("\"Yo Gandalf, how do we look?\", asked Frodo.");
println!("\"{}\" he replied", reference_eternal());
}
// ERROR: rust can't figure out where the lifetime should come from!
fn reference_eternal() -> &str {
&"Fly, you fools!"
}
Rusts's error message is pretty blunt though: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
; consider giving it a 'static lifetime
. So let's do that (run it):
fn main() {
println!("\"Yo Gandalf, how do we look?\", asked Frodo.");
println!("\"{}\" he replied", reference_eternal());
}
fn reference_eternal() -> &'static str {
&"Fly, you fools!"
}
BAM! Since the str "Fly, you fools!" is just compiled in and guaranteed to be valid for the entire life of the program, we say the lifetime is 'static
, which means FOREVER.
The other thing you need is when you could return a reference to multiple input lifetimes (run it):
fn main() {
println!("Hello darkness my old {}", pick_one(true, "friend", "frenemy"));
println!("I've come to {} with you again.", pick_one(false, "dance", "talk"));
println!("Because a {} softly creeping", pick_one(false, "gremlin", "vision"));
println!("Left its {} while I was {}.", pick_one(true, "seeds", "cell phone"), pick_one(false, "also creeping", "sleeping"));
}
// ERROR: rust doesn't know which lifetime to pick! This or that?
fn pick_one(this_or_that: bool, this: &str, that: &str) -> &str {
if this_or_that {
this
} else {
that
}
}
This situation is interesting, because it's not just one or the other. IT COULD BE EITHER! So we'll have to do something a little odd. If you put the same lifetime annotation on multiple items, rust will pick the lowest bound. So to make this work, we just tag all input references with 'a
(run it):
fn main() {
println!("Hello darkness my old {}", pick_one(true, "friend", "frenemy"));
println!("I've come to {} with you again.", pick_one(false, "dance", "talk"));
println!("Because a {} softly creeping", pick_one(false, "gremlin", "vision"));
println!("Left its {} while I was {}.", pick_one(true, "seeds", "cell phone"), pick_one(false, "also creeping", "sleeping"));
}
fn pick_one<'a>(this_or_that: bool, this: &'a str, that: &'a str) -> &'a str {
if this_or_that {
this
} else {
that
}
}
Rust will now treat the returned reference as needing to be safe for either input: 'a
is "the time when either this
or that
is deleted". Phrased differently: what pick_one
returns is only valid so long as both this
and that
are both still valid.
That's pretty much it! You now know 98% of the weird shenanigans you should need to know to use Rust. Go forth, and code!!
...What's that? You want to see some other weird stuff with lifetimes? Bluuurgh, okay, I was pretending this wasn't so difficult, but obviously you can do a lot of weird stuff with lifetimes. Want to see some of it? Okay fine. But don't say I didn't warn you!