See a typo? Have a suggestion? Edit this page on Github
Heads up This blog post series has been updated and published as an eBook by FP Complete. I'd recommend reading that version instead of these posts. If you're interested, please check out the Rust Crash Course eBook.
Arguably the biggest distinguishing aspect of Rust versus other popular programming languages is its ownership model. In this lesson, we're going to hit the basics of ownership in Rust. You can read much more in the Rust book chapter on ownership.
This post is part of a series based on teaching Rust at FP Complete. If you're reading this post outside of the blog, you can find links to all posts in the series at the top of the introduction post. You can also subscribe to the RSS feed.
Format
I'm going to be experimenting a bit with lesson format. I want to cover both:
- More theoretical discussions of ownership
- Trying to implement an actual program
As time goes on in this series, I intend to spend more time on the latter and less on the former, though we still need significant time on the former right now. I'm going to try approaching this by having the beginning of this post discuss ownership, and then we'll implement a first version of bouncy afterwards.
This format may feel a bit stilted, feedback appreciated if this approach works for people.
Comparison with Haskell
I'm going to start by comparing Rust with Haskell, since both languages have a strong concept of immutability. However, Haskell is a garbage collected language. Let's see how these two languages compare. In Haskell:
- Everything is immutable by default
- You use explicit mutability wrappers (like
IORef
orMVar
) to mark mutability - References to data can be shared however much you like
- Garbage collection frees up memory non-deterministically
- When you need deterministic resource handling (like file handles), you need to use the bracket pattern or similar
In Rust, data ownership is far more important: it's a primary aspect of the language, and allows the language to bypass garbage collection. It also allows data to often live on the stack instead of the heap, leading to better performance. Also, it runs deterministically, making it a good approach for handling other resources like file handles.
Ownership starts off with the following rules:
- Each value in Rust has a variable that’s called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value will be dropped.
Simple example
Consider this code:
#[derive(Debug)]
struct Foobar(i32);
fn uses_foobar(foobar: Foobar) {
println!("I consumed a Foobar: {:?}", foobar);
}
fn main() {
let x = Foobar(1);
uses_foobar(x);
}
Syntax note #[...]
is a compiler pragma. #[derive(...)]
is one
example, and is similar to using deriving
for typeclasses in
Haskell. For some traits, the compiler can automatically provide an
implementation if you ask it.
Syntax note {:?}
in a format string means "use the Debug
trait
for displaying this"
Foobar
is what's known as a newtype wrapper: it's a new data type
wrapping around, and with the same runtime representation of, a signed
32-bit integer (i32
).
In the main
function, x
contains a Foobar
. When it calls
uses_foobar
, ownership of that Foobar
passes to
uses_foobar
. Using that x
again in main
would be an error:
#[derive(Debug)]
struct Foobar(i32);
fn uses_foobar(foobar: Foobar) {
println!("I consumed a Foobar: {:?}", foobar);
}
fn main() {
let x = Foobar(1);
uses_foobar(x);
uses_foobar(x);
}
Results in:
error[E0382]: use of moved value: `x`
--> foo.rs:11:17
|
10 | uses_foobar(x);
| - value moved here
11 | uses_foobar(x);
| ^ value used here after move
|
= note: move occurs because `x` has type `Foobar`, which does not implement the `Copy` trait
error: aborting due to previous error
For more information about this error, try `rustc --explain E0382`.
Side note on copy trait way below...
Dropping
When a value goes out of scope, its Drop
trait (like a typeclass) is
used, and then the memory is freed. We can demonstrate this by writing
a Drop
implementation for Foobar
:
Challenge Guess what the output of the program below is before seeing the output.
#[derive(Debug)]
struct Foobar(i32);
impl Drop for Foobar {
fn drop(&mut self) {
println!("Dropping a Foobar: {:?}", self);
}
}
fn uses_foobar(foobar: Foobar) {
println!("I consumed a Foobar: {:?}", foobar);
}
fn main() {
let x = Foobar(1);
println!("Before uses_foobar");
uses_foobar(x);
println!("After uses_foobar");
}
Output
Before uses_foobar
I consumed a Foobar: Foobar(1)
Dropping a Foobar: Foobar(1)
After uses_foobar
Notice that the value is dropped before After uses_foobar
. This is
because the value was moved into uses_foobar
, and when that function
exits, the drop
is called.
Exercise 1 There's a function in the standard library,
std::mem::drop
, which drops a value immediately. Implement it.
Lexical scoping
Scoping in Rust is currently lexical, though there's a Non Lexical Lifetimes (NLL) proposal being merged in. (There's a nice explanation on Stack Overflow of what NLL does.) We can demonstrate the currently-lexical nature of scoping:
#[derive(Debug)]
struct Foobar(i32);
impl Drop for Foobar {
fn drop(&mut self) {
println!("Dropping a Foobar: {:?}", self);
}
}
fn main() {
println!("Before x");
let _x = Foobar(1);
println!("After x");
{
println!("Before y");
let _y = Foobar(2);
println!("After y");
}
println!("End of main");
}
Syntax note Use a leading underscore _
for variables that are
not used.
The output from this program is:
Before x
After x
Before y
After y
Dropping a Foobar: Foobar(2)
End of main
Dropping a Foobar: Foobar(1)
CHALLENGE Remove the seemingly-superfluous braces and run the program. Extra points: guess what the output will be before looking at the actual output.
Borrows/references (immutable)
Sometimes you want to be able to share a reference to a value without moving ownership. Easy enough:
#[derive(Debug)]
struct Foobar(i32);
impl Drop for Foobar {
fn drop(&mut self) {
println!("Dropping a Foobar: {:?}", self);
}
}
fn uses_foobar(foobar: &Foobar) {
println!("I consumed a Foobar: {:?}", foobar);
}
fn main() {
let x = Foobar(1);
println!("Before uses_foobar");
uses_foobar(&x);
uses_foobar(&x);
println!("After uses_foobar");
}
Things to notice:
uses_foobar
now takes a value of type&Foobar
, which is "immutable reference to aFoobar
."- Inside
uses_foobar
, we don't need to explicitly dereference thefoobar
value, this is done automatically by Rust. - In
main
, we can now usex
in two calls touses_foobar
- In order to create a reference from a value, we use
&
in front of the variable.
Challenge When do you think the Dropping a Foobar:
message gets
printed?
Remember from the last lesson that there is special syntax for a
parameter called self
. The signature of drop
above looks quite
different from uses_foobar
. When you see &mut self
, you can think
of it as self: &mut Self
. Now it looks more similar to
uses_foobar
.
Exercise 2 We'd like to be able to use object syntax for
uses_foobar
as well. Create a method use_it
on the Foobar
type
that prints the I consumed
message. Hint: you'll need to do this
inside impl Foobar { ... }
.
Multiple live references
We can change our main
function to allow two references to x
to
live at the same time. This version also adds explicit types on the
local variables, instead of relying on type inference:
fn main() {
let x: Foobar = Foobar(1);
let y: &Foobar = &x;
println!("Before uses_foobar");
uses_foobar(&x);
uses_foobar(y);
println!("After uses_foobar");
}
This is allowed in Rust, because:
- Multiple read-only references to a variable cannot result in any data races
- The lifetime of the value outlives the references to it. In other
words, in this case,
x
lives at least as long asy
.
Let's see two ways to break this.
Reference outlives value
Remember that std::mem::drop
from before? Check this out:
fn main() {
let x: Foobar = Foobar(1);
let y: &Foobar = &x;
println!("Before uses_foobar");
uses_foobar(&x);
std::mem::drop(x);
uses_foobar(y);
println!("After uses_foobar");
}
This results in the error message:
error[E0505]: cannot move out of `x` because it is borrowed
--> foo.rs:19:20
|
16 | let y: &Foobar = &x;
| - borrow of `x` occurs here
...
19 | std::mem::drop(x);
| ^ move out of `x` occurs here
error: aborting due to previous error
For more information about this error, try `rustc --explain E0505`.
Mutable reference with other references
You can also take mutable references to a value. In order to avoid data races, Rust does not allow value to be referenced mutably and accessed in any other way at the same time.
fn main() {
let mut x: Foobar = Foobar(1);
let y: &mut Foobar = &mut x;
println!("Before uses_foobar");
uses_foobar(&x); // will fail!
uses_foobar(y);
println!("After uses_foobar");
}
Notice how the type of y
is now &mut Foobar
. Like Haskell, Rust
tracks mutability at the type level. Yay!
Challenge
Try to guess which lines in the code below will trigger a compilation error:
#[derive(Debug)]
struct Foobar(i32);
fn main() {
let x = Foobar(1);
foo(x);
foo(x);
let mut y = Foobar(2);
bar(&y);
bar(&y);
let z = &mut y;
bar(&y);
baz(&mut y);
baz(z);
}
// move
fn foo(_foobar: Foobar) {
}
// read only reference
fn bar(_foobar: &Foobar) {
}
// mutable reference
fn baz(_foobar: &mut Foobar) {
}
Mutable reference vs mutable variable
Something I didn't explain above was the mut
before x
in:
fn main() {
let mut x: Foobar = Foobar(1);
let y: &mut Foobar = &mut x;
println!("Before uses_foobar");
uses_foobar(&x);
uses_foobar(y);
println!("After uses_foobar");
}
By default, variables are immutable, and therefore do not allow any
kind of mutation. You cannot take a mutable reference to an immutable
variable, and therefore x
must be marked as mutable. Here's an
easier way to see this:
#[derive(Debug)]
struct Foobar(i32);
fn main() {
let mut x = Foobar(1);
x.0 = 2; // changes the 0th value inside the product
println!("{:?}", x);
}
If you remove the mut
, this will fail.
Moving into mutable
This bothered me, and I assume it will bother other Haskellers. As just mentioned, the following code will not compile:
#[derive(Debug)]
struct Foobar(i32);
fn main() {
let x = Foobar(1);
x.0 = 2; // changes the 0th value inside the product
println!("{:?}", x);
}
Obviously you can't mutate x
. But let's change this ever so
slightly:
#[derive(Debug)]
struct Foobar(i32);
fn main() {
let x = Foobar(1);
foo(x);
}
fn foo(mut x: Foobar) {
x.0 = 2; // changes the 0th value inside the product
println!("{:?}", x);
}
Before learning Rust, I would have objected to this: x
is immutable,
and therefore we shouldn't be allowed to pass it to a function that
needs a mutable x
. However, this isn't how Rust views the world. The
mutability here is a feature of the variable, not the value
itself. When you move the x
into foo
, main
no longer has access
to x
, and doesn't care if it's mutated. Inside foo
, we've
explicitly stated that x
can be mutated, so we're cool.
This is fairly different from how Haskell looks at things.
Copy trait
We touched on this topic last time with numeric types vs
String
. Let's hit it a little harder. Will the following code
compile or not?
fn uses_i32(i: i32) {
println!("I consumed an i32: {}", i);
}
fn main() {
let x = 1;
uses_i32(x);
uses_i32(x);
}
It shouldn't work, right? x
is moved into uses_i32
, and then
used again. However, it compiles just fine! What gives?
Rust has a special trait, Copy
, which indicates that a type is so
cheap that it can automatically be passed-by-value. That's exactly
what happens with i32
. You can explicitly do this with the Clone
trait if desired:
#[derive(Debug, Clone)]
struct Foobar(i32);
impl Drop for Foobar {
fn drop(&mut self) {
println!("Dropping: {:?}", self);
}
}
fn uses_foobar(foobar: Foobar) {
println!("I consumed a Foobar: {:?}", foobar);
}
fn main() {
let x = Foobar(1);
uses_foobar(x.clone());
uses_foobar(x);
}
Challenge Why don't we need to use x.clone()
on the second
uses_foobar
? What happens if we put it in anyway?
Exercise 3 Change the code below, without modifying the main
function at all, so that it compiles and runs successfully. Some
hints: Debug
is a special trait that can be automatically derived,
and in order to have a Copy
implementation you also need a Clone
implementation.
#[derive(Debug)]
struct Foobar(i32);
fn uses_foobar(foobar: Foobar) {
println!("I consumed a Foobar: {:?}", foobar);
}
fn main() {
let x = Foobar(1);
uses_foobar(x);
uses_foobar(x);
}
Lifetimes
The term that goes along most with ownership is lifetimes. Every value needs to be owned, and its owned for a certainly lifetime until it's dropped. So far, everything we've looked at has involved implicit lifetimes. However, as code gets more sophisticated, we need to be more explicit about these lifetimes. We'll cover that another time.
Exercise 4
Add an implementation of the double
function to get this code to
compile, run, and output the number 2:
#[derive(Debug)]
struct Foobar(i32);
fn main() {
let x: Foobar = Foobar(1);
let y: Foobar = double(x);
println!("{}", y.0);
}
Remember: to provide a return value from a function, put -> ReturnType
after the parameter list.
Notes on structs and enums
I mentioned above that struct Foobar(i32)
is a newtype around an
i32
. That's actually a special case of a more general positional
struct, where you can have 0 or more fields, named by their numeric
position. And positions start numbering at 0, as god and Linus
Torvalds intended.
There are some more examples:
struct NoFields; // may seem strange, we might cover examples of this later
struct OneField(i32);
struct TwoFields(i32, char);
You can also use record syntax:
struct Person {
name: String,
age: u32,
}
struct
s are known as product types, which means they contain
multiple values. Rust also provides enum
s, which are sum types, or
tagged unions. These are alternatives, where you select one of the
options. A simple enum would be:
enum Color {
Red,
Blue,
Green,
}
But enums variants can also take values:
enum Shape {
Square(u32), // size of one side
Rectangle(u32, u32), // width and height
Circle(u32), // radius
}
Bouncy
Enough talk, let's fight! I want to create a simulation of a bouncing ball. It's easier to demonstrate than explain:
Let's step through the process of creating such a game together. I'll
provide the complete src/main.rs
at the end of the lesson, but
strongly recommend you implement this together with me throughout the
sections below. Try to avoid copy pasting, but instead type in the
code yourself to get more comfortable with Rust syntax.
Initialize the project
This part's easy:
$ cargo new bouncy
If you cd
into that directory and run cargo run
, you'll get output
like this:
$ cargo run
Compiling bouncy v0.1.0 (/Users/michael/Desktop/bouncy)
Finished dev [unoptimized + debuginfo] target(s) in 1.37s
Running `target/debug/bouncy`
Hello, world!
The only file we're going to touch today is src/main.rs
, which will
have the source code for our program.
Define data structures
To track the ball bouncing around our screen, we need to know the following information:
- The width of the box containing the ball
- The height of the box containing the ball
- The x and y coordinates of the ball
- The vertical direction of the ball (up or down)
- The horizontal direction of the ball (left or right)
We're going to define new datatypes for tracking the vertical and
horizontal direction, and use u32
s for tracking the
position.
We can define VertDir
as an enum
. This is a simplified version of
what enums can handle, since we aren't given it any payload. We'll do
more sophisticated stuff later.
enum VertDir {
Up,
Down,
}
Go ahead and define a HorizDir
as well that tracks whether we're
moving left or right. Now, to track a ball, we need to know its x
and y
positions and its vertical and horizontal directions. This
will be a struct, since we're tracking multiple values instead of
(like an enum) choosing between different options.
struct Ball {
x: u32,
y: u32,
vert_dir: VertDir,
horiz_dir: HorizDir,
}
Define a Frame
struct that tracks the width and height of the play
area. Then tie it all together with a Game
struct:
struct Game {
frame: Frame,
ball: Ball,
}
Create a new game
We can define a method on the Game
type itself to create a new
game. We'll assign some default width and height and initial ball
position.
impl Game {
fn new() -> Game {
let frame = Frame {
width: 60,
height: 30,
};
let ball = Ball {
x: 2,
y: 4,
vert_dir: VertDir::Up,
horiz_dir: HorizDir::Left,
};
Game {frame, ball}
}
}
Challenge Rewrite this implementation to not use any let
statements.
Notice how we use VertDir::Up
; the Up
constructor is not imported
into the current namespace by default. Also, we can define Game
with
frame, ball
instead of frame: frame, ball: ball
since the local
variable names are the same as the field names.
Bounce
Let's implement the logic of a ball to bounce off of a wall. Let's write out the logic:
- If the
x
value is 0, we're at the left of the frame, and therefore we should move right. - If
y
is 0, move down. - If
x
is one less than the width of the frame, we should move left. - If
y
is one less than the height of the frame, we should move up. - Otherwise, we should keep moving in the same direction.
We'll want to modify the ball, and take the frame as a parameter. We'll implement this as a method on the Ball
type.
impl Ball {
fn bounce(&mut self, frame: &Frame) {
if self.x == 0 {
self.horiz_dir = HorizDir::Right;
} else if self.x == frame.width - 1 {
self.horiz_dir = HorizDir::Left;
}
...
Go ahead and implement the rest of this function.
Move
Once we know which direction to move in by calling bounce
, we can
move the ball one position. We'll add this as another method within
impl Ball
:
fn mv(&mut self) {
match self.horiz_dir {
HorizDir::Left => self.x -= 1,
HorizDir::Right => self.x += 1,
}
...
}
Implement the vertical half of this as well.
Step
We need to add a method to Game
to perform a step of the game. This
will involve both bouncing and moving. This goes inside impl Game
:
fn step(&self) {
self.ball.bounce(self.frame);
self.ball.mv();
}
There are a few bugs in that implementation which you'll need to fix.
Render
We need to be able to display the full state of the game. We'll see
that this initial implementation has its flaws, but we're going to do
this by printing the entire grid. We'll add a border, use the letter
o
to represent the ball, and put spaces for all of the other areas
inside the frame. We'll use the Display
trait for this.
Let's pull some of the types into our namespace. At the top of our source file, add:
use std::fmt::{Display, Formatter};
Now, let's make sure we got the type signature correct:
impl Display for Game {
fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
unimplemented!()
}
}
We can use the unimplemented!()
macro to stub out our function
before we implement it. Finally, let's fill in a dummy main
function
that will print the initial game:
fn main () {
println!("{}", Game::new());
}
If everything is set up correctly, running cargo run
will result in
a "not yet implemented" panic. If you get a compilation error, go fix
it now.
Top border
Alright, now we can implement fmt
. First, let's just draw the top
border. This will be a plus sign, a series of dashes (based on the
width of the frame), another plus sign, and a newline. We'll
use the write!
macro, range syntax (low..high
), and a for
loop:
write!(fmt, "+");
for _ in 0..self.frame.width {
write!(fmt, "-");
}
write!(fmt, "+\n");
Looks nice, but we get a compilation error:
error[E0308]: mismatched types
--> src/main.rs:79:60
|
79 | fn fmt(&self, fmt: &mut Formatter) -> std::fmt::Result {
| ____________________________________________________________^
80 | | write!(fmt, "+");
81 | | for _ in 0..self.frame.width {
82 | | write!(fmt, "-");
83 | | }
84 | | write!(fmt, "+\n");
| | - help: consider removing this semicolon
85 | | }
| |_____^ expected enum `std::result::Result`, found ()
|
= note: expected type `std::result::Result<(), std::fmt::Error>`
found type `()`
It says "considering removing this semicolon." Remember that putting
the semicolon forces our statement to evaluate to the unit value ()
,
but we want a Result
value. And it seems like the write!
macro is
giving us a Result
value. Sure enough, if we drop the trailing
semicolon, we get something that works:
Finished dev [unoptimized + debuginfo] target(s) in 0.55s
Running `target/debug/bouncy`
+------------------------------------------------------------+
You may ask: what about all of the other Result
values from the
other calls to write!
? Good question! We'll get to that a bit later.
Bottom border
The top and bottom border are exactly the same. Instead of duplicating
the code, let's define a closure that we can call twice. We introduce
a closure in Rust with the syntax |args| { body }
. This closure will
take no arguments, and so will look like this:
let top_bottom = || {
write!(fmt, "+");
for _ in 0..self.frame.width {
write!(fmt, "-");
}
write!(fmt, "+\n");
};
top_bottom();
top_bottom();
First we're going to get an error about Result
and ()
again. You'll need to remove two semicolons to fix this. Do that
now. Once you're done with that, you'll get a brand new error
message. Yay!
error[E0596]: cannot borrow `top_bottom` as mutable, as it is not declared as mutable
--> src/main.rs:88:9
|
80 | let top_bottom = || {
| ---------- help: consider changing this to be mutable: `mut top_bottom`
...
88 | top_bottom();
| ^^^^^^^^^^ cannot borrow as mutable
The error message tells us exactly what to do: stick a mut
in the
middle of let top_bottom
. Do that, and make sure it fixes
things. Now the question: why? The top_bottom
closure has captured
the fmt
variable from the environment. In order to use that, we need
to call the write!
macro, which mutates that fmt
variable. Therefore, each call to top_bottom
is itself a
mutation. Therefore, we need to mark top_bottom
as mutable.
There are three different types of closure traits: Fn
, FnOnce
, and
FnMut
. We'll get into the differences among these in a later
tutorial.
Anyway, we should now have both a top and bottom border in our output.
Rows
Let's print each of the rows. In between the two top_bottom()
calls,
we'll stick a for
loop:
for row in 0..self.frame.height {
}
Inside that loop, we'll want to add the left border and the right border:
write!(fmt, "|");
// more code will go here
write!(fmt, "|\n");
Go ahead and call cargo run
, you're in for an unpleasant surprise:
error[E0501]: cannot borrow `*fmt` as mutable because previous closure requires unique access
--> src/main.rs:91:20
|
80 | let mut top_bottom = || {
| -- closure construction occurs here
81 | write!(fmt, "+");
| --- first borrow occurs due to use of `fmt` in closure
...
91 | write!(fmt, "|");
| ^^^ borrow occurs here
...
96 | top_bottom()
| ---------- first borrow used here, in later iteration of loop
Oh no, we're going to have to deal with the borrow checker!
Fighting the borrow checker
Alright, remember before that the top_bottom
closure capture a
mutable reference to fmt
? Well that's causing us some trouble
now. There can only be one mutable reference in play at a time, and
top_bottom
is holding it for the entire body of our method. Here's a
simple workaround in this case: take fmt
as a parameter to the
closure, instead of capturing it:
let top_bottom = |fmt: &mut Formatter| {
Go ahead and fix the calls to top_bottom
, and you should get output
that looks like this (some extra rows removed).
+------------------------------------------------------------+
||
||
||
||
...
+------------------------------------------------------------+
Alright, now we can get back to...
Columns
Remember that // more code will go here
comment? Time to replace it!
We're going to use another for
loop for each column:
for column in 0..self.frame.width {
write!(fmt, " ");
}
Running cargo run
will give you a complete frame, nice!
Unfortunately, it doesn't include our ball. We want to write a o
character instead of space when column
is the same as the ball's
x
, and the same thing for y
. Here's a partial implementation:
let c = if row == self.ball.y {
'o'
} else {
' '
};
write!(fmt, "{}", c);
There's something wrong with the output (test with cargo run
). Fix
it and your render function will be complete!
The infinite loop
We're almost done! We need to add an infinite loop in our main
function that:
- Prints the game
- Steps the game
- Sleeps for a bit of time
We'll target 30 FPS, so we want to sleep for 33ms. But how do we sleep
in Rust? To figure that out, let's go to the Rust standard library
docs and search for
sleep
. The
first result is
std::thread::sleep
,
which seems like a good bet. Check out the docs there, especially the
wonderful example, to understand this code.
fn main () {
let game = Game::new();
let sleep_duration = std::time::Duration::from_millis(33);
loop {
println!("{}", game);
game.step();
std::thread::sleep(sleep_duration);
}
}
There's one compile error in this code. Try to anticipate what it
is. If you can't figure it out, ask the compiler, then fix it. You
should get a successful cargo run
that shows you a bouncing ball.
Problems
There are two problems I care about in this implementation:
- The output can be a bit jittery, especially on a slow terminal. We
should really be using something like the
curses
library to handle double buffering of the output. - If you ran
cargo run
before, you probably didn't see it. Runcargo clean
andcargo build
to force a rebuild, and you should see the following warning:
warning: unused `std::result::Result` which must be used
--> src/main.rs:88:9
|
88 | top_bottom(fmt);
| ^^^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
I mentioned this problem above: we're ignoring failures coming from
the calls to the write!
macro in most cases but throwing away the
Result
using a semicolon. There's a nice, single character solution
to this problem. This forms the basis of proper error handling in
Rust. However, we'll save that for another time. For now, we'll just
ignore the warning.
Complete source
You can find the complete source code for this implementation as a Github gist. Reminder: it's much better if you step through the code above and implement it yourself.
I've added one piece of syntax we haven't covered yet in that tutorial, at the
end of the call to top_bottom
. We'll cover that in much more detail next
week.