I'm relatively new to Rust. This question turned out to be pretty long so I'll start with the bottom-line: Which solution do you prefer? Do you have any ideas or remarks?
My code does not compile because lines lines 6 (prev = curr
) and 12 (bar(...)
) use variables which the compiler suspects are possibly uninitialized. As the programmer, I know that there is no reason to worry because lines 6 and 12 do not run during the first iteration.
let mut curr: Enum;
let mut prev: Enum;
for i in 0..10 {
if i > 0 {
prev = curr;
}
curr = foo();
if i > 0 {
bar(&curr, &prev);
}
}
I understand that there's a limit to what you can expect from a compiler to know. So I came up with 3 different ways to answer the language's safety restriction.
1) Initialize and stop thinking too much
I could just initialize the variables with arbitrary values. The risk is that a maintainer might mistakenly think that those initial, hopefully unused, values have some important meaning. Lines 1-2 would become:
let mut curr: Enum = Enum::RED; // Just an arbitrary value!
let mut prev: Enum = Enum::BLUE; // Just an arbitrary value!
2) Add None
value to Enum
Lines 1-2 would become
let mut curr: Enum = Enum::None;
let mut prev: Enum = Enum::None;
The reason that I dislike this solution is that now I've added a new possible and unnatrural value to my enum. For my own safety, I will have to add assertion checks and match-branches to foo()
and bar()
, and any other function that uses Enum
.
3) Option<Enum>
I think that this solution is the most "by-the-book" one, but it makes the code longer and harder to comprehend.
Lines 1-2 would become
let mut curr: Option<Enum> = None;
let mut prev: Option<Enum> = None;
This time, None
belongs to Option
and not to Enum
. The innocent prev = curr
statement would become
prev = match curr {
Some(_) => curr,
None => panic!("Ah!")
};
and the naive call to bar()
would uglify to
match prev {
Some(_) => bar(&curr, &prev),
None => panic!("Oy!")
};
Questions: Which solution do you prefer? Do you have any ideas or remarks?
An uninitialized variable is a variable that has not been given a value by the program (generally through initialization or assignment). Using the value stored in an uninitialized variable will result in undefined behavior.
The value in an uninitialized variable is one of: zero, a compiler dependent value (such as 0xCC's in visual studio), or data previously stored in that memory location (old data).
"Uninitialized variables contain some value" is a incorrect statement which unfortunately is teached. A program who access an uninitialized variable has Undefined Behavior, which means it can have any behavior.
In computing, an uninitialized variable is a variable that is declared but is not set to a definite known value before it is used. It will have some value, but not a predictable one. As such, it is a programming error and a common source of bugs in software.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With