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.
In this lesson, we're going to cover what I'm dubbing the "rule of three," which applies to function parameters, iterators, and closures. We've already seen this rule applied to function parameters, but didn't discuss is so explicitly. We'll expand on parameters, and use that to launch into new information on both iterators and closures.
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.
Types of parameters
The first thing I want to deal with is a potential misconception. This may be one of those "my brain has been scrambled by Haskell" misconceptions that imperative programmers won't feel, so apologies if I'm just humoring myself and other Haskellers.
Do these two functions have the same type signature?
fn foo(mut person: Person) { unimplemented!() }
fn bar(person: Person) { unimplemented!() }
The Haskeller in me screams "they're different!" However, they're
exactly the same. The inner mutability of the person
variable in
the function is irrelevant to someone calling the function. The
caller of the function will move the Person
value into the function,
regardless of whether the value can be mutated or not. We've already
seen a hint of this: the fact that we can pass an immutable value to a
function like foo
:
fn main() {
let alice = Person { name: String::from("Alice"), age: 30 };
foo(alice); // it works!
}
With that misconception out of the way, let's consider two other similar functions:
fn baz(person: &Person) { unimplemented!() }
fn bin(person: &mut Person) { unimplemented!() }
Firstly, it's pretty easy to say that both baz
and bin
have
different signatures than foo
. These are taking references to a
Person
, not a Person
itself. But what about baz
vs bin
? Are
they the same or different? You may be tempted to follow the same
logic as foo
vs bar
and decide that the mut
is an internal
detail of the function. But this isn't true! Observe:
fn main() {
let mut alice = Person { name: String::from("Alice"), age: 30 };
baz(&alice); // this works
bin(&alice); // this fails
bin(&mut alice); // but this works
}
The first call to bin
will not compile, because bin
requires a
mutable reference, and we've provided an immutable reference. We
need to use the second version of the call. And not only does this
have a syntactic difference, but a semantic difference as well:
we've taken a mutable reference, which means we can have no other
references at the same time (remember our borrow rules from lesson 2).
The upshot of this is that there are three different ways we can pass a value into a function which appear at the type level:
- Pass by value (move semantics), like
foo
- Pass by immutable reference, like
baz
- Pass by mutable reference, like
bin
In addition, orthogonally, the variable that captures that parameters can itself be either immutable or mutable.
Mutable vs immutable pass-by-value
This one is relatively easy to see. What extra functionality do we get by having a mutable pass-by-value? The ability to mutate the value of course! Let's look at two different ways of implementing a birthday function, which increases someone's age by 1.
#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
fn birthday_immutable(person: Person) -> Person {
Person {
name: person.name,
age: person.age + 1,
}
}
fn birthday_mutable(mut person: Person) -> Person {
person.age += 1;
person
}
fn main() {
let alice1 = Person { name: String::from("Alice"), age: 30 };
println!("Alice 1: {:?}", alice1);
let alice2 = birthday_immutable(alice1);
println!("Alice 2: {:?}", alice2);
let alice3 = birthday_mutable(alice2);
println!("Alice 3: {:?}", alice3);
}
Some important takeaways:
- Our
_immutable
implementation follows a more functional idiom, creating a newPerson
value by deconstructing the originalPerson
value. This works just fine in Rust, but is not idiomatic, and potentially less efficient. - We call both versions of this function in exactly the same way, reinforcing the claim that these two functions have the same signature.
- You cannot reuse the
alice1
oralice2
values inmain
, since they've been moved during their calls. alice2
is an immutable variable, but it still gets passed in to a function which mutates it.
Mutable vs immutable pass-by-mutable-reference
This one already gets significantly harder to observe, which indicates a simple fact of Rust: it's unusual to want a mutable variable for references. The example below is very contrived, and requires playing with the more advanced concept of explicit lifetime parameters to even make it make sense. But it does demonstrate the difference between where the mut
appears.
Before we dive in: parameters that begin with a single quote ('
) are
lifetime parameters, and indicate how long a reference needs to
live. In the examples below, we're saying "the two references must
have the same lifetime." We won't cover this in more detail here, at
least not yet. If you want to learn about lifetimes, please check out
the Rust
book.
OK, let's see a difference between an immutable variable holding a mutable reference and a mutable variable holding a mutable reference!
#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
fn birthday_immutable(person: &mut Person) {
person.age += 1;
}
fn birthday_mutable<'a>(mut person: &'a mut Person, replacement: &'a mut Person) {
person = replacement;
person.age += 1;
}
fn main() {
let mut alice = Person { name: String::from("Alice"), age: 30 };
let mut bob = Person { name: String::from("Bob"), age: 20 };
println!("Alice 1: {:?}, Bob 1: {:?}", alice, bob);
birthday_immutable(&mut alice);
println!("Alice 2: {:?}, Bob 2: {:?}", alice, bob);
birthday_mutable(&mut alice, &mut bob);
println!("Alice 3: {:?}, Bob 3: {:?}", alice, bob);
}
// does not compile
fn birthday_immutable_broken<'a>(person: &'a mut Person, replacement: &'a mut Person) {
person = replacement;
person.age += 1;
}
birthday_immutable
is fairly simple. We have a mutable reference,
and we've stored it in an immutable variable. We've completely free to
mutate the value pointed to by that reference. The takeaway is: we're
mutating the value, not the variable, which is remaining the same.
birthday_mutable
is a contrived, ugly mess, but it demonstrates our
point. Here, we take two references: a person
, and a
replacement
. They're both mutable references, but person
is in a
mutable variable. The first thing we do is person = replacement;
. This changes what our person
variable is pointing at,
and does not modify the original value being pointed at by the
reference at all. In fact, when compiling this, we'll get a warning
that we never used the value passed to person
:
warning: value passed to `person` is never read
Notice that we needed to mark both alice
and bob
as mutable in
main
in this example. That's because we pass them by mutable
reference, which requires that we have the ability to mutate
them. This is different from pass-by-value with move semantics,
because in our main
function, we can directly observe the effect of
mutating the references we've passed in.
Also notice that we also have a birthday_immutable_broken
version. As you may guess from the name, it doesn't compile. We cannot
change what person
points to if it is an immutable variable.
Challenge Figure out what the output of this program is going to be before you run it.
Mutable vs immutable pass-by-immutable-reference
I'm not actually going to cover this case, since it's basically the same as the previous one. If you mark a variable as mutable, you can change which reference it holds. Feel free to play around with an example like the one above using immutable references.
Mutable to immutable
Let's point out one final bit:
fn needs_mutable(x: &mut u32) {
*x *= 2;
}
fn needs_immutable(x: &u32) {
println!("{}", x);
}
fn main() {
let mut x: u32 = 5;
let y: &mut u32 = &mut x;
needs_immutable(y);
needs_mutable(y);
needs_immutable(y);
}
From what I've told you so far, you should expect this program to fail
to compile. y
is of type &mut u32
, but we're passing it to
needs_immutable
which requires a &u32
. Type mismatch, go home!
Not so fast: since the guarantees of a mutable reference are strictly stronger than an immutable reference, you can always use a mutable reference where an immutable was needed. (Hold onto this, it will be important for closures below.)
Summary of the rule of three for parameters
There are three types of parameters:
- Pass by value
- Pass by immutable reference
- Pass by mutable reference
This is what I'm calling the rule of three. The captured variables within a function can either be mutable or immutable, which is orthogonal to the type of the parameter. However, it's by far most common to have a mutable variable with a pass-by-value. Also, at the call site, a variable must be mutable if it is called on a pass by mutable reference functions. Finally, you can use a mutable reference where an immutable was requested.
Exercise 1
Fix the program below so that it outputs the number 10. Ensure that there are no compiler warnings.
fn double(mut x: u32) {
x *= 2;
}
fn main() {
let x = 5;
double(x);
println!("{}", x);
}
Hint: you'll need to know how to dereference a reference, by putting
a asterisk (*
) in front of the variable.
Iterators
What's the output of the program below?
fn main() {
let nums = vec![1, 2, 3, 4, 5];
for i in nums {
println!("{}", i);
}
}
That's right: it prints the numbers 1 to 5. How about this one?
fn main() {
for i in 1..3 {
let nums = vec![1, 2, 3, 4, 5];
for j in nums {
println!("{},{}", i, j);
}
}
}
It prints 1,1
, 1,2
, ..., 1,5
, 2,1
, ..., 2,5
. Cool, easy
enough. Let's move nums
a bit. What does this do?
fn main() {
let nums = vec![1, 2, 3, 4, 5];
for i in 1..3 {
for j in nums {
println!("{},{}", i, j);
}
}
}
Trick question: it doesn't compile!
error[E0382]: use of moved value: `nums`
--> main.rs:4:18
|
4 | for j in nums {
| ^^^^ value moved here in previous iteration of loop
|
= note: move occurs because `nums` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait
error: aborting due to previous error
Well, that kind of makes sense. The first time we run through the
outer loop, we move the nums
value into the inner loop. Then, we
can't use the nums
value again on the second pass through the
loop. OK, logical.
Side note This was one of my personal "mind blown" moments with Rust, realizing how sophisticated lifetime tracking was to work through loops like this. Rust is pretty amazing.
We can go back to our previous version and put nums
inside the first
for
loop. That means recreating the value each time we pass through
that outer for
loop. For our little vector example, it's not a big
deal. But imagine constructing nums
was expensive. This would be a
major overhead!
If we want to avoid the move of nums
, can we get away with just
borrowing it instead? Yes we can!
fn main() {
let nums = vec![1, 2, 3, 4, 5];
for i in 1..3 {
for j in &nums {
println!("{},{}", i, j);
}
}
}
This works, but I've got a question for you: what's the type of j
?
I've got a sneaky little trick to test out different options. If you
throw this in just above the println!
call, you'll get an error
message:
let _: u32 = j;
However, this will compile just fine:
let _: &u32 = j;
By iterating over a reference to nums
, we got a reference to each
value instead of the value itself. That makes sense. Can we complete
our "rule of three" with mutable references? Yet again, yes!
fn main() {
let nums = vec![1, 2, 3, 4, 5];
for i in 1..3 {
for j in &mut nums {
let _: &mut u32 = j;
println!("{},{}", i, j);
*j *= 2;
}
}
}
Challenges First, there's a compilation error in the program above. Try to catch it before asking the compiler to help. Second, guess the output of this program before running it.
Our rule of three translates into iterators as well! We have can iterators of values, iterators of references, and iterators of mutable references. Sweet!
New nomenclature
The Vec
struct has three different methods on it that are relevant to our examples above. Starting with the mutable case, we can replace the line:
for j in &mut nums {
with
for j in nums.iter_mut() {
The signature of that method is:
pub fn iter_mut(&mut self) -> IterMut<T>
Similarly, we've got a iter()
method that can replace our immutable
reference case:
fn main() {
let nums = vec![1, 2, 3, 4, 5];
for i in 1..3 {
for j in nums.iter() {
let _: &u32 = j;
println!("{},{}", i, j);
}
}
}
And, finally, what about the iterator of values case? There, the
nomenclature is into_iter
. The idea is that we are converting the
existing value into an iterator, consuming the previous value (the
Vec
in this case) completely. This code won't compile, go ahead and
fix it by moving the let nums
statement.
fn main() {
let nums = vec![1, 2, 3, 4, 5];
for i in 1..3 {
for j in nums.into_iter() {
println!("{},{}", i, j);
}
}
}
Reexamining for loops
Here's a cool little trick I didn't mention before. for
loops are a
bit more flexible than I'd implied. The into_iter
method I mention
is actually part of a trait, appropriately named
IntoIterator
. Whenever
you use for x in y
, the compiler automatically calls into_iter()
on y
. This allows you to loop over types which don't actually have
their own implementation of Iterator
.
Exercise 2
Make this program compile by defining an IntoIterator
implementation for InfiniteUnit
. Do not define an Iterator
implementation for it. You'll probably want to define an extra
datatype. (Extra credit: also try to find a helper function in the
standard library that repeats values.)
struct InfiniteUnit;
fn main() {
let mut count = 0;
for _ in InfiniteUnit {
count += 1;
println!("count == {}", count);
if count >= 5 {
break;
}
}
}
Summary of the rule of three for iterators
Just like function parameters, iterators come in three flavors, corresponding to the following naming scheme:
into_iter()
is an iterator of values, with move semanticsiter()
is an iterator of immutable referencesiter_mut()
is an iterator of mutable references
Only iter_mut()
requires that the original variable itself be mutable.
Closures
We've danced around closures a bit throughout the crash course so far. Closures are like functions, in that they can be called on some arguments. Closures are unlike functions in that they can capture values from the local scope. We'll demonstrate this in an example, after a word of warning.
One word of warning If you're coming from a non-functional programming background, you'll likely find closures in Rust very powerful, and surprisingly common in library usage. If you come from a functional programming background, you'll likely be annoyed at how much you have to think about ownership of data when working with closures. As a Haskeller, this is still the aspect of Rust I most often get caught on. I promise, the trade-offs in the design are logical and necessary to achieve Rust's goals, but it can feel a bit onerous when compared to Haskell, or even compared to Javascript.
Alright, back to our function vs closure thing. Did you know that you can define a function inside another function?
fn main() {
fn say_hi() {
let msg: &str = "Hi!";
println!("{}", msg);
};
say_hi();
say_hi();
}
That's pretty nifty. Let's slightly refactor that:
fn main() {
let msg: &str = "Hi!";
fn say_hi() {
println!("{}", msg);
};
say_hi();
say_hi();
}
Unfortunately, Rust doesn't like that very much:
error[E0434]: can't capture dynamic environment in a fn item
--> main.rs:4:24
|
4 | println!("{}", msg);
| ^^^
|
= help: use the `|| { ... }` closure form instead
error: aborting due to previous error
Fortunately, the compiler tells us exactly how to fix it: use a closure! Let's rewrite that:
fn main() {
let msg: &str = "Hi!";
let say_hi = || {
println!("{}", msg);
};
say_hi();
say_hi();
}
We now have a closure (introduced by ||
) which takes 0
arguments. And everything just works.
Note You can shorten this a bit with let say_hi = || println!("{}", msg);
,
which is more idiomatic.
Exercise 3 Rewrite the above so that instead of taking 0
arguments, say_hi
takes a single argument: the msg
variable. Then
try out the fn
version again.
The type of a closure
What exactly is the type of say_hi
? I'm going to use an ugly trick
to get the compiler to tell us: give it the wrong type, and then try
to compile. It's probably safe to assume that a closure isn't a u32
,
so let's try this:
fn main() {
let msg: &str = "Hi!";
let say_hi: u32 = |msg| println!("{}", msg);
}
And we get the error message:
error[E0308]: mismatched types
--> main.rs:3:23
|
3 | let say_hi: u32 = |msg| println!("{}", msg);
| ^^^^^^^^^^^^^^^^^^^^^^^^^ expected u32, found closure
|
= note: expected type `u32`
found type `[closure@main.rs:3:23: 3:48]`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0308`.
[closure@main.rs:3:23: 3:48]
looks like a weird type... but let's
just give it a shot and see what happens:
fn main() {
let msg: &str = "Hi!";
let say_hi: [closure@main.rs:3:23: 3:48] = |msg| println!("{}", msg);
}
But the compiler shoots us down:
error: expected one of `!`, `(`, `+`, `::`, `;`, `<`, or `]`, found `@`
--> main.rs:3:25
|
3 | let say_hi: [closure@main.rs:3:23: 3:48] = |msg| println!("{}", msg);
| ------ ^ expected one of 7 possible tokens here
| |
| while parsing the type for `say_hi`
error: aborting due to previous error
Oh well, that isn't a valid type. What exactly is the compiler telling us then?
Anonymous types
The types of closures are anonymous in Rust. We cannot directly refer to them at all. But this leaves us in a bit of a pickle. What if we want to pass a closure into another function? For example, let's try out this program:
fn main() {
let say_message = |msg: &str| println!("{}", msg);
call_with_hi(say_message);
call_with_hi(say_message);
}
fn call_with_hi<F>(f: F) {
f("Hi!");
}
We've added a type annotation on the msg
parameter in the
closure. These are generally optional in closures, unless type
inference fails. And with our current broken code, type inference is
definitely failing. We're including it now to get better error
messages later.
We also now have a type parameter, called F
, for the closure we're
passing in. We don't know anything about F
right now, but we're
going to just try using it in a function call manner. If we compile
this, we get:
error[E0618]: expected function, found `F`
--> main.rs:8:5
|
7 | fn call_with_hi<F>(f: F) {
| - `F` defined here
8 | f("Hi!");
| ^^^^^^^^ not a function
error: aborting due to previous error
For more information about this error, try `rustc --explain E0618`.
OK, fair enough: the compiler doesn't know that F
is a function. It's time to finally introduce the magic that will make this compile: the Fn
trait!
fn call_with_hi<F>(f: F)
where F: Fn(&str) -> ()
{
f("Hi!");
}
We've now put a constraint on F
that it must be a function, which
takes a single argument of type &str
, and returns a unit
value. Actually, returning unit values is the default, so we can just
omit that bit:
fn call_with_hi<F>(f: F)
where F: Fn(&str)
{
f("Hi!");
}
Another nifty thing about the Fn
trait is that it doesn't just apply
to closures. It works on regular ol' functions too:
Exercise 4 Rewrite
say_message
as a function outside ofmain
and make the program above compile.
This was a bit boring, since say_message
isn't actually a
closure. Let's change that a bit.
fn main() {
let name = String::from("Alice");
let say_something = |msg: &str| println!("{}, {}", msg, name);
call_with_hi(say_something);
call_with_hi(say_something);
call_with_bye(say_something);
call_with_bye(say_something);
}
fn call_with_hi<F>(f: F)
where F: Fn(&str)
{
f("Hi");
}
fn call_with_bye<F>(f: F)
where F: Fn(&str)
{
f("Bye");
}
Mutable variables
Remember the good old days of visitor counters on webpages? Let's recreate that beautiful experience!
fn main() {
let mut count = 0;
for _ in 1..6 {
count += 1;
println!("You are visitor #{}", count);
}
}
That works, but it's so boring! Let's make it more interesting with a closure.
fn main() {
let mut count = 0;
let visit = || {
count += 1;
println!("You are visitor #{}", count);
};
for _ in 1..6 {
visit();
}
}
The compiler disagrees:
error[E0596]: cannot borrow immutable local variable `visit` as mutable
--> main.rs:9:9
|
3 | let visit = || {
| ----- help: make this binding mutable: `mut visit`
...
9 | visit();
| ^^^^^ cannot borrow mutably
error: aborting due to previous error
For more information about this error, try `rustc --explain E0596`.
Huh... what? Apparently calling a function counts as borrowing it. Fine, that explains why we're allowed to call it multiple times. But now we need to borrow it mutably for some reason. How come?
That reason is fairly simple: visit
has captured and is mutating a
local variable, count
. Therefore, any borrow of it is implicitly
mutably borrowing count
as well. Logically, this makes sense. But
how about at the type level? How is the compiler tracking this
mutability? To see that, let's extend this a bit further with a helper
function:
fn main() {
let mut count = 0;
let visit = || {
count += 1;
println!("You are visitor #{}", count);
};
call_five_times(visit);
}
fn call_five_times<F>(f: F)
where
F: Fn(),
{
for _ in 1..6 {
f();
}
}
We get the error message:
error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnMut`
Nice! Rust has two different traits for functions: one which covers
functions that don't mutate their environment (Fn
), and one for
functions which do mutate their environment (FnMut
). Let's try
modifying our where
to use FnMut
instead. We get one more error
message:
error[E0596]: cannot borrow immutable argument `f` as mutable
--> main.rs:16:9
|
11 | fn call_five_times<F>(f: F)
| - help: make this binding mutable: `mut f`
...
16 | f();
| ^ cannot borrow mutably
error: aborting due to previous error
For more information about this error, try `rustc --explain E0596`.
Calling this mutating function requires taking a mutable borrow of the
variable, and that requires defining the variable as mutable. Go ahead
and stick a mut
in front of the f: F
, and you'll be golden.
Multiple traits?
Is this closure a Fn
or a FnMut
?
|| println!("Hello World!");
Well, it doesn't modify any variables in the local scope, so
presumably it's an Fn
. Therefore, passing it to
call_five_times
—which expects a FnMut
—should fail,
right? Not so fast, it works just fine! Go ahead and add this line to
the program above and prove it to yourself:
call_five_times(|| println!("Hello World!"));
Every value which is a Fn
is automatically an FnMut
. This is
similar to what happens with a function parameter: if you have a
mutable reference, you can automatically use it as an immutable
reference, since the guarantees of a mutable reference are
stronger. Similarly, if we're using a function in such a way that it's
safe even if the function is mutable (FnMut
), it's certainly safe to
do the same thing with an immutable function (Fn
).
Does this sound a bit like subtyping? Good, it should :)
The rule of three?
If you've noticed, we now have two different types of functions, in a lesson entitled "the rule of three." What could possibly be coming next? We've seen functions that can be called multiple times in an immutable context, kind of like immutable references. We've seen functions that can be called multiple times in a mutable context, kind of like mutable references. That just leaves one thing... call by value/move semantics!
We're going to define a closure that moves a local variable
around. We're going to go back to use a String
instead of a
u32
, to avoid the fact that a u32
is Copy
able. And we're going
to use a weird bit of magic in the middle to force things to be moved
instead of being treated as references. We'll go into gory detail on
that trick later, and see alternatives.
fn main() {
let name = String::from("Alice");
let welcome = || {
let name = name; // here's the magic
println!("Welcome, {}", name);
};
welcome();
}
Alright, name
is moved into the welcome
closure. This is forced
with the let name = name;
bit. Still not 100% convinced that name
was actually moved in? Watch this:
fn main() {
let name1 = String::from("Alice");
let welcome = || {
let mut name2 = name1;
name2 += " and Bob";
println!("Welcome, {}", name2);
};
welcome();
}
name1
is defined as immutable. But name2
is mutable, and we do
in fact successfully mutate it. This can only happen if we pass by
value instead of by reference. Want further proof? Try to use name1
again after we've defined welcome
.
The third function trait
Let's complete our rule of three. Remember our call_five_times
?
Let's use it on welcome
:
fn main() {
let name = String::from("Alice");
let welcome = || {
let mut name = name;
name += " and Bob";
println!("Welcome, {}", name);
};
call_five_times(welcome);
}
fn call_five_times<F>(f: F)
where
F: Fn(),
{
for _ in 1..6 {
f();
}
}
And we get a brand new error message, this time referencing FnOnce
:
error[E0525]: expected a closure that implements the `Fn` trait, but this closure only implements `FnOnce`
--> main.rs:4:19
|
4 | let welcome = || {
| ^^ this closure implements `FnOnce`, not `Fn`
5 | let mut name = name;
| ---- closure is `FnOnce` because it moves the variable `name` out of its environment
...
10 | call_five_times(welcome);
| --------------- the requirement to implement `Fn` derives from here
error: aborting due to previous error
For more information about this error, try `rustc --explain E0525`.
Replacing Fn()
with FnOnce()
should fix the compilation, right? Wrong!
error[E0382]: use of moved value: `f`
--> main.rs:18:9
|
18 | f();
| ^ value moved here in previous iteration of loop
|
= note: move occurs because `f` has type `F`, which does not implement the `Copy` trait
error: aborting due to previous error
For more information about this error, try `rustc --explain E0382`.
Our loop ends up calling f
multiple times. But each time we call
f
, we're moving the value. Therefore, the function can only be
called once. Maybe that's why they named it FnOnce
.
Let's rewrite this to have a helper function that only calls things once:
fn main() {
let name = String::from("Alice");
let welcome = || {
let mut name = name;
name += " and Bob";
println!("Welcome, {}", name);
};
call_once(welcome);
}
fn call_once<F>(f: F)
where
F: FnOnce(),
{
f();
}
That works just fine. Hurrah!
Further function subtyping
Previously, we said that every Fn
is also an FnMut
, since anywhere
you can safely call a mutable function, you can also call an immutable
function. It turns out that every Fn
and every FnMut
are also
FnOnce
s, because any context you can guarantee the function will
only be called once is safe for running functions with mutable or
immutable environments.
The move keyword
There's a subtle point we're about to get into, which I didn't understand till I wrote this lesson (thanks to Sven Marnach for the explanation there). The Rust by Example section on closures was the best resource for helping it all click for me. I'll do my best here explaining it myself.
Functions accept parameters explicitly, complete with type signatures. You're able to explicitly state whether a parameter is pass by value, mutable reference, or immutable reference. Then, when you use it, you're able to choose any of the weaker forms available. For example, if you pass a parameter by mutable reference, you can later use it by immutable reference. However, you cannot use it by value:
fn pass_by_value(_x: String) {}
fn pass_by_ref(_x: &String) {}
fn pass_by_mut_ref(x: &mut String) {
pass_by_ref(x); // that's fine
pass_by_value(*x); // that's a paddlin'
}
fn main() {}
Closures accept parameters, but they make the type annotations optional. If you omit them, they are implicit. In addition, closures allow you to capture variables. These never take a type annotation; they are always implicit. Nonetheless, there needs to be some concept of how these values were captured, just like we need to know how parameters are passed into a function.
How a value is captured will imply the same set of borrow rules we're used to in Rust, in particular:
- If by reference, then other references can live concurrently with the closure
- If by mutable reference, then as long as the closure is alive, no other references to the values can exist. However, once the closure is dropped, other references can exist again.
- If by value, then the value cannot be used by anything ever again. (This automatically implies that the closure owns the value.)
However, there's an important and (dare I repeat myself) subtle distinction between closures and functions:
Closures can own data, functions cannot
Sure, if you pass by value to a function, the function call takes ownership of the data during execution. But closures are different: the closure itself can own data, and use it while it is being called. Let's demonstrate:
fn main() {
// owned by main
let name_outer = String::from("Alice");
let say_hi = || {
// force a move, again, we'll get smarter in a second
let name_inner = name_outer;
println!("Hello, {}", name_inner);
};
// main no longer owns name_outer, try this:
println!("Using name from main: {}", name_outer); // error!
// but name_inner lives on, in say_hi!
say_hi(); // success
}
Try as you might, you could not achieve the same thing with a plain
old function, you'd need to keep name_outer
alive separately and
then pass it in.
Alright, let's get to that smarter way to force a move. In the closure
above, we have let name_inner = name_outer;
. This forces the closure
to use name_outer
by value. Since we use by value, we can only call
this closure once, since it fully consumes name_outer
on the first
call. (Go ahead and try adding a second say_hi()
call.) But in
reality, we're only using the name by immutable reference inside the
closure. We should be able to call it multiple times. If we skip the
forced use by value, we can use by reference, leaving the name_outer
in the original scope:
fn main() {
// owned by main
let name_outer = String::from("Alice");
let say_hi = || {
// use by reference
let name_inner = &name_outer;
println!("Hello, {}", name_inner);
};
// main still owns name_outer, this is fine
println!("Using name from main: {}", name_outer); // success
// but name_inner lives on, in say_hi!
say_hi(); // success
say_hi(); // success
}
However, if we change things around a bit, so that name_outer
goes
out of scope before say_hi
, everything falls apart!
fn main() {
let say_hi = { // forcing the creation of a smaller scope
// owned by the smaller scope
let name_outer = String::from("Alice");
// doesn't work, closure outlives captured values
|| {
// use by reference
let name_inner = &name_outer;
println!("Hello, {}", name_inner);
}
};
// syntactically invalid, name_outer isn't in this scope
//println!("Using name from main: {}", name_outer); // error!
say_hi();
say_hi();
}
What we need is some way to say: I'd like the closure to own the
values it captures, but I don't want to have to force a use by value
to do it. That will allow a closure to outlive the original scope of
the value, but still allow a closure to be called multiple times. And
to do that, we introduce the move
keyword:
fn main() {
let say_hi = { // forcing the creation of a smaller scope
// owned by the smaller scope
let name_outer = String::from("Alice");
// now it works!
move || {
// use by reference
let name_inner = &name_outer;
println!("Hello, {}", name_inner);
}
};
// syntactically invalid, name_outer isn't in this scope
//println!("Using name from main: {}", name_outer); // error!
say_hi();
say_hi();
}
The ownership of name_outer
passes from the original scope to the
closure itself. We still only use it by reference, and therefore we
can call it multiple times. Hurrah!
One final bit here. Using the move
keyword like this moves all
captured variables into the closure, and therefore they cannot be used
after the closure. For example, this will fail to compile:
fn main() {
let name = String::from("Alice");
let _ = move || println!("Hello, {}", name);
println!("Using name from main: {}", name); // error!
}
Reluctant Rust
Alright, one final point before we sum things up and dive into examples. The type of capture is implicit in a closure. How does Rust decide whether to capture by value, mutable reference, or immutable reference. I like to think of Rust as being reluctant here: it strives to capture the weakest way possible. To paraphrase the Rust by Example book:
Closures will preferentially capture by immutable reference, then by mutable reference, and only then by value.
In our previous examples with let name_inner = name_outer;
, we
forced Rust to capture by value. However, it doesn't like doing that,
and will instead capture by reference (mutable or immutable) if it can
get away with that. It does this based on the strongest kind of usage
for that value. That is:
- If any part of the closure uses a variable by value, it must be captured by value.
- Otherwise, if any part of the closure uses a variable by mutable reference, it must be captured by mutable reference.
- Otherwise, if any part of the closure uses a variable by immutable reference, it must be captured by immutable reference.
It does this reluctant capturing even if it causes the program to fail to compile. Capturing by reference instead of value can cause lifetime issues, as we've seen previously. However, Rust does not look at the full context of the usage of the closure to determine how to capture, it only looks at the body of the closure itself.
But, since there are many legitimate cases where we want to force a
capture by value to solve lifetime issues, we have the move
keyword
to force the issue.
Side note It may be a little annoying at times that Rust doesn't
just look at your program as a whole and guess that you want that
move
added. However, I think it's a great decision in the language:
that kind of "do what I mean" logic is fragile and often times
surprising.
Recap: ownership, capture, and usage
To recap the salient points:
- Within a closure, a variable can be used by value, mutable reference, or immutable reference
- In addition, all variables captured by a closure can be captured by value, by mutable reference, or by immutable reference
- We cannot use a variable in a stronger way than it was captured. If it was captured by mutable reference, it can be used by immutable reference, but not by value.
- To solve lifetime issues, we can force a closure to capture by value
with the
move
keyword. - Short of the
move
keyword, Rust will be reluctant, and capture in the weakest way allowed by the body of the closure. - Regarding the traits of closures:
- If a closure uses anything by value, then the closure is a
FnOnce
- Otherwise, if a closure uses anything by mutable reference, then
the closure is a
FnMut
, which automatically impliesFnOnce
as well - Otherwise, a closure is a
Fn
, which automatically implies bothFnMut
andFnOnce
- If a closure uses anything by value, then the closure is a
I consider the points above complicated enough that I'm included a number of further examples to help hammer the points home. These are inspired heavily by the Rust by Example examples.
For all of the examples below, I'm going to assume the presence of the following three helper functions in the source:
fn call_fn<F>(f: F) where F: Fn() {
f()
}
fn call_fn_mut<F>(mut f: F) where F: FnMut() {
f()
}
fn call_fn_once<F>(f: F) where F: FnOnce() {
f()
}
Examples
Consider this main
function:
fn main() {
let name = String::from("Alice");
let say_hi = || println!("Hello, {}", name);
call_fn(say_hi);
call_fn_mut(say_hi);
call_fn_once(say_hi);
}
name
lives longer than say_hi
, and therefore there's no problem
with the closure keeping an immutable reference to name
. Since it
only has immutable references to the environment and consumes no
values, say_hi
is a Fn
, FnMut
, and FnOnce
, and the code above
compiles.
// bad!
fn main() {
let say_hi = {
let name = String::from("Alice");
|| println!("Hello, {}", name)
};
}
By contrast, this example won't compile. name
will go out of scope
once we leave the curly braces. However, our closure is capturing it
by reference, and so the reference outlives value. We could do our
trick from before of forcing it to capture by value:
fn main() {
let say_hi = {
let name = String::from("Alice");
|| {
let name = name;
println!("Hello, {}", name)
}
};
//call_fn(say_hi);
//call_fn_mut(say_hi);
call_fn_once(say_hi);
}
But this only implements a FnOnce
, since the value is captured and
consumed, preventing it from being run again. There's a better way!
Instead, we can force the closure to take ownership of name
, but
still capture by reference:
fn main() {
let say_hi = {
let name = String::from("Alice");
move || println!("Hello, {}", name)
};
call_fn(&say_hi);
call_fn_mut(&say_hi);
call_fn_once(&say_hi);
}
Now we're back to having a Fn
, FnMut
, and FnOnce
! To avoid the
say_hi
value itself from being moved with each call, we now pass a
reference to the call_fn
functions. I believe (though am not 100%
certain) that this wasn't necessary in the first example since, above,
there was no captured environment, and therefore the closure could be
Copy
ed. This closure, with a captured environment, cannot be
Copy
ed`.
fn main() {
let say_hi = {
let name = String::from("Alice");
|| std::mem::drop(name)
};
//call_fn(say_hi);
//call_fn_mut(say_hi);
call_fn_once(say_hi);
}
This example uses the drop
function to consume name
. Since we use
by value, we must capture by value, and therefore must take ownership
of the value. As a result, sticking move
at the front of the closure
is unnecessary, though it will do no harm.
fn main() {
let mut say_hi = {
let mut name = String::from("Alice");
move || {
name += " and Bob";
println!("Hello, {}", name);
}
};
//call_fn(say_hi);
call_fn_mut(&mut say_hi);
call_fn_once(&mut say_hi);
}
Using the +=
operator on a String
requires a mutable reference, so
we're out of the territory of immutable reference capturing. Rust
will fall back to capturing via mutable reference. That requires that
the name
also be declared mutable. And since name
will go out of
scope before the closure, we need to move
ownership to the
closure. And since calling say_hi
will mutate data, we need to put a
mut
on its declaration too.
When we pass say_hi
to the call functions, we need to use &mut
to
ensure (1) the value isn't moved, and (2) the value can be
mutated. Also, call_fn
is invalid here, since our closure is FnMut
and FnOnce
, but not Fn
.
Challenge What will the output of this program be? How many times
do we add the string " and Bob"
to name
?
fn main() {
let mut name = String::from("Alice");
let mut say_hi = || {
name += " and Bob";
println!("Hello, {}", name);
};
//call_fn(say_hi);
call_fn_mut(&mut say_hi);
call_fn_once(&mut say_hi);
}
We can also avoid the capture by letting the name
live longer than the closure.
// bad!
fn main() {
let mut name = String::from("Alice");
let mut say_hi = || {
name += " and Bob";
println!("Hello, {}", name);
};
//call_fn(say_hi);
call_fn_mut(&mut say_hi);
call_fn_once(&mut say_hi);
println!("And now name is: {}", name);
}
Adding the println!
at the end, which references name
, is invalid,
since say_hi
is still in scope. This is due to lexical
lifetimes. You can turn on the (at time of writing) experimental
feature non-lexical lifetimes by adding #![feature(nll)]
to the top
of your source code. Or, you can explicitly use braces to denote the
scope of the closure:
fn main() {
let mut name = String::from("Alice");
{
let mut say_hi = || {
name += " and Bob";
println!("Hello, {}", name);
};
//call_fn(say_hi);
call_fn_mut(&mut say_hi);
call_fn_once(&mut say_hi);
}
println!("And now name is: {}", name);
}
You can also (perhaps somewhat obviously) use a value in multiple different ways:
fn main() {
let mut name = String::from("Alice");
let mut say_hi = || {
println!("Hello, {}", name); // use by ref
name += " and Bob"; // use by mut ref
std::mem::drop(name); // use by value
};
//call_fn(say_hi);
//call_fn_mut(say_hi);
call_fn_once(say_hi);
}
In these cases, the most powerful use determines the kind of capture we need. Since we used by value above, we must also capture by value, and therefore must take ownership.
Which trait to use?
It may be intimidating to try and think through which of these three traits you need. You can usually punt on this and let the compiler yell at you. To quote the Rust book:
Most of the time when specifying one of the
Fn
trait bounds, you can start withFn
and the compiler will tell you if you needFnMut
orFnOnce
based on what happens in the closure body.
I'd give a slightly different piece of advice, following the dictum of
"be lenient in what you accept." When receiving functions as
arguments, the most lenient thing to start with is a FnOnce
. If your
usage turns out to be more restrictive, then listen to the
compiler.
For more information on closures as output parameters, see Rust by Example's chapter.
Summary of the rule of three for closures
Both functions and closures are annotated using the Fn
family of
trait bounds. These form a subtyping relationship, where every Fn
is
also an FnMut
, and every FnMut
is also an FnOnce
.
FnOnce
works like pass by valueFnMut
works like pass by mutable referenceFn
works like pass by immutable reference
How these captured variables are used by the closure determines which
of these three it is. Since functions, by definition, never capture
local variables, they are always Fn
.
Exercise 5
Putting together what we've learned about iterators and closures,
modify line 5 below (the one starting with for i in
) so that the
program prints the numbers 2,4,6,..,20
twice.
fn main() {
let nums: Vec<u32> = (1..11).collect();
for _ in 1..3 {
for i in nums.map(unimplemented!()) {
println!("{}", i);
}
}
}
GUIs and callbacks
What better way to tie this all off than by writing a GUI and some callbacks? I'm going to use GTK+ and the wonderful gtk-rs set of crates. Our goal ultimately is to create a GUI with a single button on it. When that button is clicked, a message will be written to a file that says "I was clicked."
For this example, you'll definitely want to use a cargo project. Go ahead and run:
$ cargo new clicky
$ cd clicky
Now add gtk as a dependency. Within the [dependencies]
section of
the Cargo.toml
, add the line:
gtk = "0.5"
And now we're going to rip off the sample code from gtk-rs's
website. Put this in your main.rs
(bonus points if you type it
yourself instead of copy-pasting):
extern crate gtk;
use gtk::prelude::*;
use gtk::{Button, Window, WindowType};
fn main() {
if gtk::init().is_err() {
println!("Failed to initialize GTK.");
return;
}
let window = Window::new(WindowType::Toplevel);
window.set_title("First GTK+ Program");
window.set_default_size(350, 70);
let button = Button::new_with_label("Click me!");
window.add(&button);
window.show_all();
window.connect_delete_event(|_, _| {
gtk::main_quit();
Inhibit(false)
});
button.connect_clicked(|_| {
println!("Clicked!");
});
gtk::main();
}
Assuming you've got all of your system libraries set up correctly,
running cargo run
should get you a nice, simple GUI.
If you do have trouble installing the crates, check out gtk-rs's requirements page first.
Replacing the callback
You may have noticed that sample code already includes a callback,
which prints Clicked!
to stdout each time the button is
clicked. That certainly makes our life a little bit easier. Now,
inside of that callback, we need to:
- Open up a file
- Write some data to the file
We're going to take a first stab at this without doing any error
handling. Instead, we'll use .unwrap()
on all of the Result
values, causing our program to panic!
if something goes wrong. We'll
clean that up a bit later.
Searching the standard library for
file quickly finds
std::fs::File
,
which seems promising. It also seems like the create
function will
be the easiest way to get started. We'll write to mylog.txt
. The example at the top of that page shows write_all
(thanks Rust for awesome API docs!). First, try out this bit of code:
let mut file = std::fs::File::create("mylog.txt");
file.write_all(b"I was clicked.\n");
After addressing exercise 6 below, you'll see this error message:
error[E0599]: no method named `write_all` found for type `std::fs::File` in the current scope
--> src/main.rs:27:14
|
27 | file.write_all(b"I was clicked.\n");
| ^^^^^^^^^
|
= help: items from traits can only be used if the trait is in scope
help: the following trait is implemented but not in scope, perhaps add a `use` for it:
|
3 | use std::io::Write;
Oh, that's something new. In order to use items from a trait, the
trait has to be in scope. Easy enough, we can just add use std::io::Write;
to our closure:
use std::io::Write;
let mut file = std::fs::File::create("mylog.txt");
file.write_all(b"I was clicked.\n");
Exercise 6 If you're following along with the code like you should
be, you probably got a different error message above, and the code
I've provided here doesn't actually fix everything. You need to add an
extra method call to convert a Result<File, Error>
into a
File
. Hint: I mentioned it above.
Go ahead and run this program (via cargo run
), click the button a
few times, and close the window. Then look at the contents of
mylog.txt
. No matter how many times you clicked, you'll only get one
line of output.
The problem is that each time the callback is called, we call create
from File
, which overwrites the old file. One approach here would be
to create an appending file handle (awesome bonus exercise for anyone
who wants to take it on). We're going to take another approach.
Share the file
Let's move our create
call to outside of the closure
definition. We'll open the file in the main
function itself, the
closure can capture a mutable reference to the file
, and all will be
well in the world.
Unfortunately, the compiler really dislikes this:
error[E0596]: cannot borrow `file` as mutable, as it is a captured variable in a `Fn` closure
--> src/main.rs:28:9
|
28 | file.write_all(b"I was clicked.\n");
| ^^^^ cannot borrow as mutable
|
help: consider changing this to accept closures that implement `FnMut`
--> src/main.rs:26:28
|
26 | button.connect_clicked(|_| {
| ____________________________^
27 | | use std::io::Write;
28 | | file.write_all(b"I was clicked.\n");
29 | | });
| |_____^
error[E0597]: `file` does not live long enough
--> src/main.rs:28:9
|
26 | button.connect_clicked(|_| {
| --- value captured here
27 | use std::io::Write;
28 | file.write_all(b"I was clicked.\n");
| ^^^^ borrowed value does not live long enough
...
32 | }
| - `file` dropped here while still borrowed
|
= note: borrowed value must be valid for the static lifetime...
error: aborting due to 2 previous errors
Some errors occurred: E0596, E0597.
For more information about an error, try `rustc --explain E0596`.
error: Could not compile `clicky`.
Or more briefly:
cannot borrow `file` as mutable, as it is a captured variable in a `Fn` closure
help: consider changing this to accept closures that implement `FnMut`
`file` does not live long enough
note: borrowed value must be valid for the static lifetime...
We can understand both of these by looking at the signature for connect_clicked
:
fn connect_clicked<F: Fn(&Self) + 'static>(&self, f: F) -> SignalHandlerId
connect_clicked
is a method which takes some function f
of type
F
and returns a SignalHandlerId
. We're not using that return
value, so just ignore it. The function is a Fn
. Therefore, we're
not allowed to pass in a FnMut
or an FnOnce
. GTK must be allowed
to call that function multiple times without the restrictions of a
mutable context. So keeping a mutable reference won't work.
The other interesting thing is + 'static
. We briefly mentioned
lifetime parameters above. 'static
is a special lifetime parameter,
which means "can live for the entire lifetime of the program." As one
nice example of this, all string literals have type &'static str
,
though we usually just write &str
.
The problem is that our file
does not have 'static
lifetime. It
is created in the main
function, remains in the main
function, and
only lives as long as the main
function. You may argue that the
main
function lives the entire length of the program, but that's not
exactly true. In our example above, button
will outlive file
when
calling drop
s (since drop
s are performed in FILO order). If the
drop
for a button decided to call the click
callback one more time, we'd have memory unsafety.
So what we're left with is: we need a closure which does not have a mutable reference to local data. How do we do that?
Move it
We can get the compiler to stop complaining about the lifetime by
moving the variable into the closure. Now we're guaranteed that the
file
will live as long as the closure itself, meeting the guarantees
demanded by 'static
. Do accomplish this, stick move
in front of
the closure.
This still doesn't solve our Fn
issue, however. How can we allow our
callback to be called multiple times after moving the value in?
Reference counting (hint: nope)
We've reached a point where the normal borrow rules of Rust simply aren't enough. We cannot prove to the compiler that our callback will obey the mutable reference rules: exactly one mutable reference will exist at a given time. These kinds of situations occur often enough that the standard library provides built in support for reference counted types.
Add the following statement to the top of your main.rs
:
use std::rc::Rc;
An Rc
is a single threaded reference counted value. There's also an
Arc
type, which is atomic, and can be used in multithreaded
applications. Since GTK is a single-threaded library, we're safe using
an Rc
instead of an Arc
. One really awesome thing about Rust is
that if you make a mistake about this, the compiler can catch
you. This is because Rc
does not implement the Sync
and Send
traits. See more in the
Send
documentation.
Anyway, back to our example. We can wrap up our original file
with
reference counting with this:
let file = std::fs::File::create("mylog.txt").unwrap();
let file = Rc::new(file);
How do we then get access to the underlying File
to use it? Turns
out: we don't need to do anything special. Keeping our original
file.write_all
does what we want. This is because Rc
implements
the Deref
trait:
impl<T> Deref for Rc<T> {
type Target = T;
...
}
This means that you can get a reference to a T
from a Rc<T>
. Since
method call syntax automatically takes a reference, everything
works. Nice.
Well, almost everything:
error[E0596]: cannot borrow data in a `&` reference as mutable
--> src/main.rs:32:9
|
32 | file.write_all(b"I was clicked.\n");
| ^^^^ cannot borrow as mutable
Reference counting allows us to have multiple references to a value, but they're all immutable references. Looks like we haven't actually made our situation any better than before, where we had ensured that the single owner of our data was the closure.
RefCell
RefCell
is designed to exactly solve this problem. I'm not going to
go into detail explaining it, because the API docs for std::cell
do
that better than I
could. I recommend you
go read that intro now, come back and work on this code, and then go
read the docs again. Personally, I had to read that explanation about
4 or 5 times and bash my head against some broken code before it
finally sank in correctly.
Anyway, add use std::cell::RefCell;
, and then wrap a RefCell
around the original File
:
let file = std::fs::File::create("mylog.txt").unwrap();
let file = RefCell::new(file);
Now our code will fail to compile with a different message:
error[E0599]: no method named `write_all` found for type `std::cell::RefCell<std::fs::File>` in the current scope
--> src/main.rs:29:14
|
29 | file.write_all(b"I was clicked.\n").unwrap();
|
Unlike Rc
, with RefCell
we cannot rely on the Deref
implementation to get us a File
. Instead, we'll need to use a method
on RefCell
to get a reference to the File
:
file.borrow().write_all(b"I was clicked.\n");
But that doesn't quite work:
error[E0596]: cannot borrow data in a `&` reference as mutable
Fortunately, that fix is as easy as using borrow_mut()
instead. And
now our program works, hurray!
NOTE Often, reference counting (Rc
or Arc
) and cells (Cell
,
RefCell
, or Mutex
) go hand in hand, which is why my first instinct
in writing this lesson was to use both an Rc
and a
RefCell
. However, in this case, it turns out that just the RefCell
is sufficient.
Exercise 7
The error handling in this program is lackluster. There are three problems:
- If
gtk::init()
fails, the exit code of our program is still0
(indicating success). - If opening
mylog.txt
fails, we panic. - If writing to the file fails, we panic.
To fix this, have main
return a value of type Result<(), Box<std::error::Error>>
. Most other errors can be automatically
coerced via From::from
into Box<std::error::Error>
. For problems
(1) and (2), use the standard error handling mechanisms we discussed
back in lesson 3. For problem (3), print an error message with
eprintln!
when an error occurs.
Fearless concurrency!
It's finally time to do some fearless concurrency. We're going to write a program which will:
- Allocate a string containing the word "Fearless"
- Fork a thread every second for 10 iterations
- In the forked thread:
- Add another exclamation point to the string
- Print the string
Before we begin, you can probably identify some complex pieces of ownership that are going to go on here:
- Multiple threads will have access to some mutable data
- We need to ensure only one writer at a time
- We need to ensure that the data is released when everyone is done with it
Instead of trying to design a great solution to this from the beginning, we'll treat this like a proper crash course. We'll do the most naive stuff possible, look at the error messages, and try to improve. If you think you can implement the complete program yourself now, definitely give it a shot! Even if you don't think you can implement it yourself, it's worth trying. The effort will make the explanation below more helpful.
Introducing the functions
We're going to use the following three functions:
std::thread::spawn
to spawn a thread. It has an interesting signature:
pub fn spawn<F, T>(f: F) -> JoinHandle<T> where
F: FnOnce() -> T,
F: Send + 'static,
T: Send + 'static,
The Send
trait means that both the provided function and its return
value must be values which can be sent to a different thread. The
'static
bit says that we cannot retain any references to local
variables. And the FnOnce()
bit says that any closure will work.
std::thread::sleep
to have the main thread sleep. It takes a value of type Duration
,
which brings us to our last function:
std::time::Duration::new
takes the number of seconds and nanoseconds in a duration.
Before we introduce the great fun which is spawning a new thread, let's try a single threaded version:
use std::thread::sleep;
use std::time::Duration;
fn main() {
let mut msg: String = String::from("Fearless");
for _ in 1..11 {
msg.push('!');
println!("{}", msg);
sleep(Duration::new(1, 0));
}
}
We can even wrap up that msg.push
and println!
in a closure to get
a bit closer to the call to spawn
:
use std::thread::sleep;
use std::time::Duration;
fn main() {
let mut msg: String = String::from("Fearless");
for _ in 1..11 {
let inner = || {
msg.push('!');
println!("{}", msg);
};
inner();
sleep(Duration::new(1, 0));
}
}
That gives us an error message:
error[E0596]: cannot borrow immutable local variable `inner` as mutable
Go ahead and fix that and make this compile.
Introducing spawn
The simplest way to introduce spawn is to replace the inner()
call
with spawn(inner)
. Replace:
use std::thread::sleep;
with
use std::thread::{sleep, spawn};
And add the spawn
call. We get the error message:
error[E0373]: closure may outlive the current function, but it borrows `msg`, which is owned by the current function
--> main.rs:7:25
|
7 | let mut inner = || {
| ^^ may outlive borrowed value `msg`
8 | msg.push('!');
| --- `msg` is borrowed here
help: to force the closure to take ownership of `msg` (and any other referenced variables), use the `move` keyword
|
7 | let mut inner = move || {
| ^^^^^^^
error: aborting due to previous error
Seems simple enough: we have to have a self contained closure to pass
to spawn
, which can't refer to values from the parent thread. Let's
just add a move
in front of the closure. We get an error message:
error[E0382]: capture of moved value: `msg`
--> main.rs:8:13
|
7 | let mut inner = move || {
| ------- value moved (into closure) here
8 | msg.push('!');
| ^^^ value captured here after move
|
= note: move occurs because `msg` has type `std::string::String`, which does not implement the `Copy` trait
I still don't find these error messages particularly enlightening. But it's telling us that we're trying to capture a moved value. This is happening because we're moving the value into the closure in the first iteration of the loop, and then trying to move it in again. That clearly won't work!
A broken solution
Let's just cheat and create a new copy of the string for each
iteration. That's easy enough: add the following above let mut inner
:
let mut msg = msg.clone();
This will compile (with a warning) and run, but it has the wrong output. We aren't adding extra exclamation points each time. We're not actually dealing with shared mutable data. Darn.
But that cloning gives me another idea...
Reference counting
Maybe we can throw in that reference counting we mentioned previously, and let each thread keep a pointer to the same piece of data.
use std::thread::{sleep, spawn};
use std::time::Duration;
use std::rc::Rc;
fn main() {
let msg = Rc::new(String::from("Fearless"));
for _ in 1..11 {
let mut msg = msg.clone();
let mut inner = move || {
msg.push('!');
println!("{}", msg);
};
spawn(inner);
sleep(Duration::new(1, 0));
}
}
Well, that's a new one:
error[E0277]: `std::rc::Rc<std::string::String>` cannot be sent between threads safely
--> main.rs:13:9
|
13 | spawn(inner);
| ^^^^^ `std::rc::Rc<std::string::String>` cannot be sent between threads safely
|
There's that fearless concurrency we've heard so much about! The
compiler is preventing us from sending an Rc
value between
threads. It would be nice if the compiler mentioned it, but we already
know that for multithreaded applications, we need an atomic reference
counter, or std::sync::Arc
. Go ahead and switch over to that. You
should get a new error message:
error[E0596]: cannot borrow immutable borrowed content as mutable
--> main.rs:10:13
|
10 | msg.push('!');
| ^^^ cannot borrow as mutable
error: aborting due to previous error
Inner mutability
Above, I mentioned that Rc
and RefCell
usually go together. The
Rc
provides reference counting, and the RefCell
provides
mutability. Maybe we can combine Arc
and RefCell
too?
use std::thread::{sleep, spawn};
use std::time::Duration;
use std::sync::Arc;
use std::cell::RefCell;
fn main() {
let msg = Arc::new(RefCell::new(String::from("Fearless")));
for _ in 1..11 {
let mut msg = msg.clone();
let mut inner = move || {
let msg = msg.borrow_mut();
msg.push('!');
println!("{}", msg);
};
spawn(inner);
sleep(Duration::new(1, 0));
}
}
More fearless concurrency:
error[E0277]: `std::cell::RefCell<std::string::String>` cannot be shared between threads safely
--> main.rs:15:9
|
15 | spawn(inner);
| ^^^^^ `std::cell::RefCell<std::string::String>` cannot be shared between threads safely
|
= help: the trait `std::marker::Sync` is not implemented for `std::cell::RefCell<std::string::String>`
= note: required because of the requirements on the impl of `std::marker::Send` for `std::sync::Arc<std::cell::RefCell<std::string::String>>`
= note: required because it appears within the type `[closure@main.rs:10:25: 14:10 msg:std::sync::Arc<std::cell::RefCell<std::string::String>>]`
= note: required by `std::thread::spawn`
You could go search for more info, but the normal way to have a
mutable, multithreaded cell is a Mutex
. Instead of borrow_mut()
,
we have a lock()
method, which ensures that only one thread at a
time is using the mutex. Let's try that out:
use std::thread::{sleep, spawn};
use std::time::Duration;
use std::sync::{Arc, Mutex};
fn main() {
let msg = Arc::new(Mutex::new(String::from("Fearless")));
for _ in 1..11 {
let mut msg = msg.clone();
let mut inner = move || {
let msg = msg.lock();
msg.push('!');
println!("{}", msg);
};
spawn(inner);
sleep(Duration::new(1, 0));
}
}
We get the error:
error[E0599]: no method named `push` found for type `std::result::Result<std::sync::MutexGuard<'_, std::string::String>, std::sync::PoisonError<std::sync::MutexGuard<'_, std::string::String>>>` in the current scope
Oh, right. lock
ing can fail, due to something called
poisoning. (Check out the
docs
for more information.) To quote the docs:
Most usage of a mutex will simply
unwrap()
these results, propagating panics among threads to ensure that a possibly invalid invariant is not witnessed.
This is the closest to runtime exceptions I've seen the Rust docs
mention, nice. If we add that .unwrap()
, we get told that msg
needs to be mutable. And if we add mut
, we've written our first
multithreaded Rust application using shared mutable state.
Notice how the compiler prevented us from making some serious concurrency mistakes? That's pretty awesome.
As a final step, see which mut
s and move
s you can and cannot
remove from the final program. Make sure you can justify to yourself
why the compiler does or does not accept each change.
Next time
You're now deep into the hard parts of Rust. What comes now is getting more comfortable with the hairy bits of ownership and closures, and to get more comfortable with the library ecosystem. We're ready to get much more real world next time, and learn about tokio, the de facto standard async I/O framework in Rust.
Rust at FP Complete | Introduction