Logically yet illegaly uninitialized variable in Rust
I am also very new to Rust, but I find the third option clearest. I will add that unwrap()
or expect()
can be used in place of the match expressions with the same meaning ("I know this is occupied"):
let mut curr: Option<Enum> = None;
let mut prev: Option<Enum> = None;
for i in 0..10 {
if i > 0 {
prev = curr;
}
curr = Some(foo());
if i > 0 {
bar(curr.as_ref().unwrap(), prev.as_ref().unwrap());
}
}
See the playground. It still has a little extra ceremony to it.
Generally I find longer real life examples of loops in this form hard to follow; the structure of the loop obscures the desired logic. If one can structure it more like an iteration, I find it clearer:
for (prev, curr) in FooGenerator::new().tuple_windows().take(10) {
bar(&prev, &curr);
}
See the playground.
For what it's worth, a simple way of fixing your code is to pull the first iteration out of the loop. Because of the if
statements, the only thing the first iteration does is calling foo()
. Additionally, you can move the assignment to prev
to the end of the loop, so you can scope curr
to the inside of the loop:
let mut prev = foo();
for _ in 1..10 {
let curr = foo();
bar(&curr, &prev);
prev = curr;
}
Note that I changed the range to start at 1 instead of 0, since we pulled the first iteration out of the loop.
I don't think this approach is better than the one in the accepted answer, but I do think it's quite simple and easy to understand.
Where possible, avoid using too many mutable variable bindings in Rust, especially in loops. Managing state within a loop using mutating variables can make code harder to understand and lead to bugs. It's usually a red flag to see a counter variable in a for
loop, which can very often be replaced with an iterator over values instead.
In your case, you can create an iterator over the values produced by foo
, and pass them in pairs to bar
, using tuple_windows
from the itertools
crate:
use itertools::Itertools as _; // 0.9.0
use std::iter::repeat_with;
for (prev, curr) in repeat_with(foo).tuple_windows().take(9) {
bar(&curr, &prev);
}
Note that you can't make a mistake here and forget to set prev
or curr
, and future maintainers can't accidentally swap two lines and mess it up either.
This style is very succinct and expressive. For example, the way I've written the snippet above emphasises that bar
will be called 9 times. You could also modify it slightly if you preferred to emphasise that foo
will be called 10 times:
for (prev, curr) in repeat_with(foo).take(10).tuple_windows() {
bar(&curr, &prev);
}
Actually, the logic that you are expressing is what is called a “fold” and you could be using a fold instead of a loop.
What one could do is:
(0..9).map(|_|foo()) //an iterator that outputs foo() 9 times, not 10
.fold(foo(), |prev, curr| {bar(&curr, &prev); curr});
The business end is of course the line with .fold
in it. What a fold does is that it takes a single argument for an initial value, calls a binary function on that initial value and the current value the iterator produces, and then uses the result as the new initial value for the next iteration. I have simply used a function here that is called for the side-effect as you have, and as resulting value use the curr
value, which is thus used as the initial value of the next iteration, and serves as prev
.
The final value returned by this expression is the last curr
, which can of curse be ignored.
https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.fold