lucidbrot
lucidbrot

Reputation: 6156

I query an optional component mutably in bevy and it compiles but does not modify it. Why?

The unofficial bevy cheat book explains how queries work:

The first type parameter for a query is the data you want to access. Use & for shared/readonly access and &mut for exclusive/mutable access. Use Option if the component is not required (you want to access entities with or without that component). If you want multiple components, put them in a tuple.

It implies that I could specify something like this to modify an Optional component and also seeing the entities where it is None:

fn my_mut_system(query: Query<(&Person, Option<&mut Name>)>) {
  // I can mutate the Name here, if the person has one.
}

But i couldn't get that to work. (Solution at the end of the question).
While attempting, I ran into an example that compiles but does the wrong thing, and I would like to understand what is going on.


What i Tried

Readonly Optional works:

fn my_second_system(query: Query<(&Person, Option<&Name>)>) {
    for (_person,name) in &query {
        println!("Hello Again, {:?}", name);
    }
}

Fetching only the components that have a Name, also works:

// Fetching Name directly: works
fn my_update_system(query: Query<&Name, With<Person>>) {
    for name in &query {
        println!("Hello, {}", name.0);
    }
}

And if I add a lot of mut keywords, I can even mutate the name:

fn my_update_system(mut query: Query<&mut Name, With<Person>>) {
    for mut name in &mut query {
        println!("Hello, {}. I'll call you Bernhard.", name.0);
        name.0 = "Bernhard".to_string();
    }
}

That all works. The name change is reflected in further systems.

But when I try to also receive the components that do not have a name, I struggle:

// This builds but does the wrong thing!
fn my_mut_system(query: Query<(&Person, Option<&mut Name>)>) {
    for (_person, mut name_option) in &query {
        if let Some(ref mut name) = &mut name_option {
            println!("Hello a third time, {:?}", name);
            *name = &Name("Renamedinho".to_string());
        } else {
            println!("Hello, Anon.");
        }
    }
}

This example builds and runs without warnings, but the next system (chained after this my_mut_system) does not see the name change.

The same code, with the type hints that rust-analyzer adds in vscode:
_person is of type &Person, name_option of type Option, and name of type &mut &Name

I thought what should happen here is this: I get an Option that contains a mutable reference to a Name. If I follow that reference with *name =, I can assign a new Name into the Option. But clearly, that is not what is happening, since the compiler required a *name = &Name instead of the *name = Name I was expecting...

Note that trying to avoid that roundabout does not compile:

// This does not work!
fn my_mut_system_one(query: Query<(&Person, Option<&mut Name>)>) {
    for (_person, mut name_option) in &query {
        // Note: maybe I could even drop both of these ref mut and &mut ?
        // The confusion_two.rs file seems to indicate so.
        if let Some(name) = &mut name_option {
            println!("Hello a third time, {:?}", name);
            name.0 = "Renamedinho One".to_string();
        } else {
            println!("Hello, Anon.");
        }
    }
}

Even though the type Option<&mut Name> contains mut, the compiler says:

error[E0594]: cannot assign to name.0, which is behind a & reference

error[E0594]: cannot assign to `name.0`, which is behind a `&` reference
  --> src/main.rs:63:13
   |
63 |             name.0 = "Renamedinho One".to_string();
   |             ^^^^^^ cannot assign

For more information about this error, try `rustc --explain E0594`.
error: could not compile `wobbler-repro` (bin "wobbler-repro") due to 1 previous error

In this gist is an example snippet + example output of what i mentioned above.

Solution

Making the whole query mut allows this:

// This seems to work
fn my_mut_system_two(mut query: Query<(&Person, Option<&mut Name>)>) {
    for (_person, mut name_option) in &mut query {
        if let Some(name) = &mut name_option {
            println!("Hello a third time, {:?}", name);
            name.0 = "Succeederinho".to_string();
        } else {
            println!("Hello another time, Anon.");
        }
    }
}

Question

Upvotes: 0

Views: 402

Answers (1)

lucidbrot
lucidbrot

Reputation: 6156

Based on the comments of Chayim Friedman and cafce25, I can now guess at the answers:

In the line below,

        if let Some(ref mut name) = &mut name_option {

a name_option of type &Name ("Reference to Name") is bound mutably. Apparently that results not in a &mut Name, but in a &mut &Name ("Mutable Reference to Reference to Name").

  • Why does my_mut_system not modify the name permanently?

In the line below, which I naively let some previous compiler messages guide me to,

 *name = &Name("Renamedinho".to_string());

We dereference the &mut &Name on the left-hand side and get *name of type &Name. We can obviously not assign to (*name).0, since (*name) is an immutable reference. That is why my_mut_system_one did not compile.

When we now assign a new &Name to *name, we overwrite only the mut reference, not what it points to.

Why did this compile, if it does not actually have mut access to the query and the option?

Because apparently that's just how the language syntax works. Sometimes, having a way to locally take a mutable reference to some immutable reference can be useful, so it can't be an error.


Later edit because I still don't fully get it and writing it out might help:

I read this rust github discussion to learn that ref mut means "reference to a mutable something" and has the same effect as &mut on the right side of the equal sign. So I believe what happened is:

  • I am looping over &query. That means I take the reference to query and the loop does some magic to loop over query but return references to the individual entries.
  • Then the mut name_option ends up being non-mutable, because it can not be when we don't have &mut query. This is the first place where a compiler error would be appropriate imo.
    • We can see in the screenshot that mut name_option is of type Option<&Name>
  • I thought I match this mutably with = &mut name_option, but actually I am simply taking a mutable reference to this Option, so now it is a &mut &Option<&Name>.
  • This is now pattern-matched to the left-hand side Some(ref mut name).
    • Pattern-matching a right-hand side &Some(xyz) does some more magic (Using the Magic Ergonomics RFC to become a Some(&xyz) instead.
    • I am wildly guessing that this rule can be applied twice in succession, to turn my &mut &Option<&Name> into a Option<&mut & &Name>
    • And then the ref mut name says that name should be a reference. So the &mut inside the Option disappears, leaving me with name: & &Name.
  • The line *name = &Name(...) dereferences the reference name: &&Name. So *name is of type &Name.
    • And there it stores a new instance of Name.
    • Something still does not make sense here, because there was no mut left in my explanation... so why would it be able to assign that?

In conclusion, I will have to rephrase my question once I've re-read the rust book.

Upvotes: 0

Related Questions