Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Logically yet illegaly uninitialized variable in Rust

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?

like image 593
Jonathan Jacobson Avatar asked Aug 05 '20 21:08

Jonathan Jacobson


People also ask

Why is my variable uninitialized?

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.

What value is stored in uninitialized variable?

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).

Why are uninitialized variables bad?

"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.

What is an uninitialized variable set to?

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.


2 Answers

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);
}
like image 88
Peter Hall Avatar answered Nov 03 '22 02:11

Peter Hall


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

like image 41
Zorf Avatar answered Nov 03 '22 01:11

Zorf