Abstract
A language that doesn't affect the way you think about programming is not worth knowing. -- Alan J. PerlisRust enhances it's variables with lifetime information, which I believe is unique among languages currently in production. This is the most exciting and best language feature I've encountered since recursion. It makes me think about aspects of a program I wasn't thinking about before and catches heisenbugs at compile time. I hope that explaining that to you will help you catch that excitement.
Some motivation
My motivation for looking into new programming languages is to try and become more productive, mostly by catching more bugs at compile time that are hard to find in a debugger. Functional languages bring two features that help with this: immutable variables that help prevent timing-related locking bugs, and algebraic data types that keep you from using error indicators as if they were data.While Rust isn't a pure functional language like Haskell, it has both immutable variables and algebraic data types, so it makes the cut as far as I'm concerned. Even better, it has ownership, which prevents a wide variety of pointer bugs. That's the third major source of heisenbugs. Garbage collectors also do this, but can leak memory, and have a significant run-time cost that prevents them from being used in embedded systems with limited resources.
Rust ownership keeps track of the single variable that owns a value. When that variable goes out of scope, the value is freed. Reference variables can borrow a value. When those variables go out of scope, the borrow .
Some examples
So let's look at just about the simplest case where this shows up.A buggy example
I have to apologize for using C here. But I need to demonstrate the type of bug Rust's lifetimes keeps you from writing.So what do you think the following code outputs?
#include <stdio.h>
int main() {
int *x;
{
int y = 1;
x = &y;
}
{
int z = 3;
int *y = &z;
printf("y points to the value %d\n", *y);
}
printf("x points to the value %d\n", *x);
}
-Wall
and -Wextra
just gets a warning about y being unused.This is a classic bad memory reference bug -
x
points to memory that's been freed when the printf
runs. The second block serves no purpose but to overwrite that value to surface the bug.Now in Rust
So let's rewrite this example in Rust. We don't need the second block for reasons that will become obvious shortly.fn main() {
let x;
{
let y = 1;
x = &y;
}
println!("x is now {}", x)
}
error: `y` does not live long enough --> 1-error.rs:6:5| 5 | x = &y; | - borrow occurs here 6 | } | ^ `y` dropped here while still borrowed 7 | println!("x is now {}", x) 8 | } | - borrowed value needs to live until here
The first line of the error - and Rust errors are very informative, which sometimes leads to them being long - says that the value assigned to y doesn't live long enough. The next line tells us it has to live as long as the variable
x
- the "block suffix" is the rest of the block following the statement. The third line says it only lives as long as the value y
, with similar wording. So indeed, not long enough.
This is the heart of lifetimes in Rust. The compiler keeps track of how long any value will be valid, and issues a warning if we try saving a reference to that value that could outlive the value. This prevents the same type of bugs that a garbage collector eliminates, but it's all done in the compiler, so there's no runtime overhead, meaning Rust is suitable for the same kind of platforms as C.
But this doesn't work. Here's the error message:
By assigning
And we see the same error as before:
And it works fine.A bad fix
So let's fix the error. We'll just take the block delimiters out so both values live to the end of the function:fn main() {
let x;
let y = 1;
x = &y;
println!("x is now {}", x)
}
error: `y` does not live long enough
--> 1-error.rs:7:1
|
4 | x = &y;
| - borrow occurs here
...
7 | }
| ^ `y` dropped here while still borrowed
|
= note: values in a scope are dropped in the opposite order they are created
We get pretty much the same error message. This illustrates an annoyance with lifetimes - variables are freed in the reverse order of declaration, and that's enough of an issue that the compiler complains.A good fix
Fortunately, this is easy to fix: Just swap the two variable declarations.fn main() {
let y = 1;
let x = &y;
println!("x is now {}", x)
}
x
after y
is created, the lifetimes are fine.Function calls
This is nice, but not all that interesting. So let's extend it to an external function:fn id(x: &u32) -> &u32 {
return &x;
}
fn main() {
let x;
let c1 = 1;
x = id(&c1);
println!("x is now {}", x)
}
error: `c1` does not live long enough
--> 2-function.rs:11:1
|
9 | x = id(&c1);
| -- borrow occurs here
10 | println!("x is now {}", x)
11 | }
| ^ `c1` dropped here while still borrowed
|
= note: values in a scope are dropped in the opposite order they are created
Checking the same fix of swapping the two variable declarations:
fn id(x: &u32) -> &u32 {
return &x;
}
fn main() {
let c1 = 1;
let x = id(&c1);
println!("x is now {}", x)
}
Names
As you can see, in this case the lifetime of the input value is used for the lifetime of the function's return value. Let's try an example without an input value:fn id() -> &u32 {
return &0;
}
fn main() {
let x = id();
println!("x is now {}", x)
}
error[E0106]: missing lifetime specifier
--> 3-zeroadic.rs:1:12
|
1 | fn id() -> &u32 {
| ^ expected lifetime parameter
|
= help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
= help: consider giving it a 'static lifetime
Here's another example of Rust's nice error messages, suggesting a `static lifetime.
fn id() -> &'static u32 { return &0; } fn main() { let x = id(); println!("x is now {}", x) }
No comments:
Post a Comment