Why can I call .iter() on an immutable empty array?

Very good points, thank you. I guess my real question then is, why would I ever want to define an immutable, empty array? In my code above, could u ever be useful?

It might happen in the case where I am working on the code, and am going to modify the array later.

Another case where it could be useful would be e.g. where I want to build a json object using serde, but I know that in this case the list is always empty. This could be because I am calling an API and the field is required, but I never use it.

#[derive(Serialize)]
struct MyStruct {
    always_empty: [SomeType; 0],
    other_field_i_actually_use: String,
}

Or it could be useful with a generic function that takes something which can be turned into an iterator, and in one of the calls, I want to provide it an empty iterator.

Or it could be useful in code generation in the output of e.g. a macro.

4 Likes

It's not like you purposefully write empty literal arrays directly in code. What if you are parsing JSON and it contains an empty array? What if you are interfacing with a database and you get back an empty result set? Dynamically allowing an empty array is useful for these kinds of reasons, not because you necessarily want to write for item in &[].

4 Likes

I mean, there are other cases that are genuinely never useful such as:

1 + 1;

The above computes 2, then throws it away. But it is allowed because language-wise, it is the same as

a_method_that_first_prints_and_then_returns_an_int();

and the above is obviously sometimes useful. Disallowing 1 + 1; would be a weird special case, although I could imagine that you might add a warning for it.

6 Likes

Another thing to compare to is that I sometimes write this:

if false {
    some_code_i_want_to_disable_for_now();
}

The equivalent of the above for a loop is:

for x in [] {
    some_code_i_want_to_disable_for_now();
}
3 Likes

Thanks! This is really just a testament to how awesome the compiler is - in my short time programming in Rust I've come to depend on the compiler to not let me make mistakes by not handling some eventuality in my code so I was surprised when it allowed me to iterate over a collection that could be empty, and do stuff with the non-existent items in the collection, without warning me that it could panic.

It cannot panic. Iterating over an array with 0 items performs the loop 0 times.

Empty sets are normal and common in everyday life. "You have no money in your account." "There are zero cars in this parking garage." "Yes, we have no bananas today." It's not a program error for a sequence to be empty; it would be far more weird if it was.

There's even a standard iterator whose job is to always be empty. This comes in handy, for example, in generic code where an iterator is required but the caller doesn't actually have any items to provide.

5 Likes

But it cannot panic. By the very definition of a for loop, if the iterated-over collection is empty, the loop body simply never executes. Why would it panic?

In my case the error was:

thread 'main' panicked at 'index out of bounds: the len is 0 but the index is 0',...

The code looked something like this:

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Doc {
    pub dh_doc_id: String,
    pub doc_name: String
}

#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Res {
    pub docs: Vec<Doc>,
}

let res: Vec<Res> = // call API, deserialize JSON to struct

for doc in res[0].docs.iter() {
// do some stuff
}

The way that I "fixed" it was to simply wrap the for loop in an if statement that checks the length of res.

It's res[0] that causes that panic, not for doc in ... If you rewrote it like this

let r = &res[0];
for doc in r.docs.iter() {

the panic would happen on the first line because res is empty. .iter() never gets called in this code at all.

6 Likes

In the olden days of Fortran 66, the body of a loop was always executed.

      let j = 0
      do 10 i = 1, 0
10    j = j + 1

The variable j would end up with the value 1. The result was some hard to find bugs. The problem was fixed with the introduction of Fortran 77. Unfortunately, that change broke all of my programs. (It didn't break any of my wife's because she was a professional programmer.)

5 Likes

For what it's worth, the indexing error you ran into is something I intellectually think shouldn't be allowed - i.e. indexing is an operation that can panic - intellectually I think indexing should return an Option unless statically provable (e.g. correct bounds checking, something akin to what TypeScript can do with type guards). The slice .get method always gives an Option so there's that.

Practically however, I understand that many many many people disagree with that, because array indexing is fundamental to (maybe) all imperative programming languages, and if you don't check your bounds you are a "bad person".

Anyway, there is a lint that can warn you in some cases: Clippy Lints it's not enabled by default so you have to add it.

1 Like

Good idea :grinning_face_with_smiling_eyes:

warning: unused arithmetic operation that must be used
 --> src/main.rs:2:5
  |
2 |     1 + 1;
  |     ^^^^^
  |
  = note: `#[warn(unused_must_use)]` on by default

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=9fa2d8226c624665d3f6e0db3ddd83e5

"Bad person" is definitely too strong. Of late I've been talking about places with algorithmic invariants as being where dogmatically returning Option/Result can hurt more than it helps. That's not unlike what you said about "statically provable", but acknowledging that actually putting that proof machinery in the language may be impractical.

Related previous conversations:

Or, more generally, To panic or to Result - #4 by scottmcm

2 Likes

I have places where I use .get and others indexing depending on the context. I don't like to have too many unwrap()s because I feel I must include a comment of why it's safe.

let foo = vec![1, 2, 3];
let second = foo.get(1).unwrap() // Unwrap safe because of previous line

let bar = [1; 3];
let n = 1;
let my_index = MyIndex::new(n, bar.len())?;  // Error if n > bar.len()
let second = bar[my_index.u()]; // my_index.u() returns a usize

Of course the first example is pretty silly, and the second example looks awkward. The issue in the first example is when foo is mutable and modified far from the get(). For the second example, in my real code I create an index once and can pass it around all over the place with the assurance that indexing will never panic.

In this case there is a lint we can add, the body of the for loop will always be dead code.

3 Likes

You may want to define immutable empty array in code if you are working with sample data, or want to initialize something.

For example, imagine having some sort of "friendship" graph and you initialize it like so:

fn set_friends(name: &str, friends: &[&str]) {
    println!("{} is friends with ... {}", name, friends.join(", "));
}

fn main() {
    set_friends("Adam", &["Bonnie", "Cara"]);
    set_friends("Doug", &[]); // Doug has no friends :-(
}

Unfortunately, Doug has no friends and this satisfies the desire to specify empty arrays. :slight_smile:

1 Like

In that case, I would suggest modifying the code to include an additional function:

fn set_friends(name: &str, friends: &[&str]) {
    println!("{} is friends with ... {}", name, friends.join(", "));
}

fn set_location(name: &str, location: &str) {
    println!("{} is at {}", name, location);
}

fn main() {
    set_friends("Adam", &["Bonnie", "Cara"]);
    set_friends("Doug", &[]); // Doug has no friends :-(

    set_location("Doug", &"the bar");
}
1 Like

Another source of intuition is to think about how the collected output of an iterator can serve as input to others.

The “interface” that enables the composition is a list. It comes “for free” with every collection of items. Keep in mind, composition of “containers”, not items.

[] composed with [1,2,3] = something valid

From a mathematical perspective, [] is required to be valid just as 0 is for addition (for a fun deep-dive, check out monoid).

Finally, when composing with operations that return an item from the collection (e.g., max, min, find) the following correspondence avoids the repeated need for an if or match statement:

[] ~ None // failure (it’s ok to fail, no need to panic :))
_  ~ Some(_) // success 
1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.