Skip to content

Use distinct types for query "data" generics and query "filter" generics #7680

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
alice-i-cecile opened this issue Feb 14, 2023 · 4 comments · Fixed by #9918
Closed

Use distinct types for query "data" generics and query "filter" generics #7680

alice-i-cecile opened this issue Feb 14, 2023 · 4 comments · Fixed by #9918
Labels
A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use D-Complex Quite challenging from either a design or technical perspective. Ask for help!
Milestone

Comments

@alice-i-cecile
Copy link
Member

What problem does this solve or what need does it fill?

It's easy for users to accidentally mix up their "data" (Q) and "filter" (F) generics on Query.

For example:

  • Query<(&A, With<B>): forces ugly unpacking.
  • Query<(&A, Without<B>): likely seriously wrong.
  • Query<Changed<T>): probably not what the user wants.
  • Query<(), Entity>: nonsensical
  • Query<&A, &B>: very weird, probably broken

See #7679 for an example of this footgun in the wild.

What solution would you like?

  1. Add WorldQueryData: WorldQuery and WorldQueryFilter: WorldQuery.
  2. Split derive(WorldQuery) macro into two parts.
  3. Only implement WorldQueryData / WorldQueryFilter for the correct WorldQuery types.

What alternative(s) have you considered?

We could use a custom linter #1602, but a) this catches errors later / less elegantly and b) we don't have one yet.

@alice-i-cecile alice-i-cecile added A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use D-Complex Quite challenging from either a design or technical perspective. Ask for help! labels Feb 14, 2023
@alice-i-cecile alice-i-cecile added this to the 0.11 milestone Feb 14, 2023
@wainwrightmark
Copy link
Contributor

Hi, I've had a quick go at this and gotten all the tests and examples working - there's no logic changes, just changing some types and type parameters. I haven't done any of the "fun" bits like fixing documentation and writing tests yet.

I've added WorldQueryData: WorldQuery and WorldQueryFilter: WorldQuery, implemented them for the appropriate types, and forced you to use them in the left and right hand sides of the query respectively.

The thing I'm unsure of is how the derive macros ought to work.
In bevy 0.10 you do one of these

#[derive(WorldQuery)]
pub struct DataQuery {
    a: &'static A,
}

#[derive(WorldQuery)]
#[world_query(mutable)]
pub struct MutDataQuery {
    pub a: &'static mut A,
}

#[derive(WorldQuery)]
struct FilterQuery{
    a: With<A>,
}

and it generates implementations of WorldQuery and ReadOnlyWorldQuery (on the same struct, unless you specify mutable). But with the split, we need a way of specifying whether to generate WorldQueryFilter or WorldQueryData or, in principle, both.

Should we use two different macros like this:

#[derive(WorldQueryData)]
pub struct DataQuery {
    a: &'static A,
}

#[derive(WorldQueryData)]
#[world_query(mutable)]
pub struct MutDataQuery {
    pub a: &'static mut A,
}

#[derive(WorldQueryFilter)]
struct FilterQuery{
    a: With<A>,
}

That seems good but I worry that that makes it impossible to derive both for the same struct. (Both macros would generate a WorldQuery impl and they would clash). Is that a use case we want to support? I think it's probably only possible for trivial or generic types anyway but I worry that doing it this way would take away that possibility.

The other options would be:

#[derive(WorldQuery)]
#[world_query(data)]
pub struct DataQuery {
    a: &'static A,
}

#[derive(WorldQuery)]
#[world_query(mutable)] //data is implied by mutable
pub struct MutDataQuery {
    pub a: &'static mut A,
}

#[derive(WorldQuery)]
#[world_query(filter)]
struct FilterQuery{
    a: With<A>,
}

or

#[derive(WorldQuery, WorldQueryData)]
pub struct DataQuery {
    a: &'static A,
}

#[derive(WorldQuery, WorldQueryData)]
#[world_query(mutable)]
pub struct MutDataQuery {
    pub a: &'static mut A,
}

#[derive(WorldQuery, WorldQueryFilter)]
struct FilterQuery{
    a: With<A>,
}

or possibly one I haven't thought of.

@urben1680
Copy link
Contributor

urben1680 commented Jun 17, 2023

That seems good but I worry that that makes it impossible to derive both for the same struct.

I would want to keep having this use case possible. I want to implement WorldQuery for my type that has a &mut Foo and a Without<Bar> in the same struct. Only in this combination I want to offer methods on the Item of this WorldQuery.

However, I want to point out I am still in a design phase and I might end up not needing this. Others might though.

@alice-i-cecile
Copy link
Member Author

I like the two distinct WorldQueryData versus WorldQueryFilter macros here: I think it's much clearer.

I would want to keep having this use case possible. I want to implement WorldQuery for my type that has a &mut Foo and a Without in the same struct. Only in this combination I want to offer methods on the Item of this WorldQuery.

Yeah, this is an important pattern to consider how to support. We could implement WorldQueryData for the filter types as well, but have them fetch () instead, and be omitted from the query item completely?

My personal experience has been that these types are only really used as the only type in Query: we could instead encourage people to use the SystemParam derive 🤔 But that won't give us a nice way to implement methods on the Item associated type, which is really what you tend to need.

@wainwrightmark
Copy link
Contributor

I like the two distinct WorldQueryData versus WorldQueryFilter macros here: I think it's much clearer.

Agreed. After trying the different options, I think this is definitely the best one.

I would want to keep having this use case possible. I want to implement WorldQuery for my type that has a &mut Foo and a Without in the same struct. Only in this combination I want to offer methods on the Item of this WorldQuery.

The changes I've tried so far do prevent that pattern from working so it would be good to find a way to keep it.

Yeah, this is an important pattern to consider how to support. We could implement WorldQueryData for the filter types as well, but have them fetch () instead, and be omitted from the query item completely?

We could have something like FilterDataAdapter<F: WorldQueryFilter> : WorldQueryData which could then be used by the macro if you mark filters with an attribute.

#[derive(WorldQuery)]
#[world_query(mutable)]
pub struct FooWithoutBar{
    foo: &'static mut Foo,
    #[filter]
    filter: Without<Bar>
}

The macro could then exclude the filter items from the returned Item.

The problem with that approach is that it might be difficult to get to work for Changed<T> and Added<T> (which current do not work correctly if used in the Data position). It also relies on the implementations of WorldQueryData and WorldQueryFilter being similar and this restriction could block hypothetical future enhancements or optimizations that diverge the implementations.

Another option might be to have some sort of FilteredItem<Q, F> type which contained the data plus the type information of the filter. Queries would have a method as_filtered that returned items of that type and you could implement methods on it.

impl FilteredItem<&'static mut Foo, Without<Bar>>
{
    fn baz(self){
        todo!()
    }
}

fn baz_all_foos_without_bar(mut query: Query<&'static mut Foo, Without<Bar>>)
{
   for item in query.as_filtered()
   {
    item.baz();
   }
}

This way keeps the separation of data and filter and it's quite nice in that you don't need to use the derive macro to take advantage of it. That said, it is a bit less ergonomic and I think it would require major refactoring of Query (moving all of the iter* methods onto a trait.

@alice-i-cecile alice-i-cecile modified the milestones: 0.11, 0.12 Jun 19, 2023
@mockersf mockersf modified the milestones: 0.12, 0.13 Oct 20, 2023
github-merge-queue bot pushed a commit that referenced this issue Nov 28, 2023
# Objective

- Fixes #7680
- This is an updated for #8899
which had the same objective but fell a long way behind the latest
changes


## Solution

The traits `WorldQueryData : WorldQuery` and `WorldQueryFilter :
WorldQuery` have been added and some of the types and functions from
`WorldQuery` has been moved into them.

`ReadOnlyWorldQuery` has been replaced with `ReadOnlyWorldQueryData`. 

`WorldQueryFilter` is safe (as long as `WorldQuery` is implemented
safely).

`WorldQueryData` is unsafe - safely implementing it requires that
`Self::ReadOnly` is a readonly version of `Self` (this used to be a
safety requirement of `WorldQuery`)

The type parameters `Q` and `F` of `Query` must now implement
`WorldQueryData` and `WorldQueryFilter` respectively.

This makes it impossible to accidentally use a filter in the data
position or vice versa which was something that could lead to bugs.
~~Compile failure tests have been added to check this.~~

It was previously sometimes useful to use `Option<With<T>>` in the data
position. Use `Has<T>` instead in these cases.

The `WorldQuery` derive macro has been split into separate derive macros
for `WorldQueryData` and `WorldQueryFilter`.

Previously it was possible to derive both `WorldQuery` for a struct that
had a mixture of data and filter items. This would not work correctly in
some cases but could be a useful pattern in others. *This is no longer
possible.*

---

## Notes

- The changes outside of `bevy_ecs` are all changing type parameters to
the new types, updating the macro use, or replacing `Option<With<T>>`
with `Has<T>`.

- All `WorldQueryData` types always returned `true` for `IS_ARCHETYPAL`
so I moved it to `WorldQueryFilter` and
replaced all calls to it with `true`. That should be the only logic
change outside of the macro generation code.

- `Changed<T>` and `Added<T>` were being generated by a macro that I
have expanded. Happy to revert that if desired.

- The two derive macros share some functions for implementing
`WorldQuery` but the tidiest way I could find to implement them was to
give them a ton of arguments and ask clippy to ignore that.

## Changelog

### Changed
- Split `WorldQuery` into `WorldQueryData` and `WorldQueryFilter` which
now have separate derive macros. It is not possible to derive both for
the same type.
- `Query` now requires that the first type argument implements
`WorldQueryData` and the second implements `WorldQueryFilter`

## Migration Guide

- Update derives

```rust
// old
#[derive(WorldQuery)]
#[world_query(mutable, derive(Debug))]
struct CustomQuery {
    entity: Entity,
    a: &'static mut ComponentA
}

#[derive(WorldQuery)]
struct QueryFilter {
    _c: With<ComponentC>
}

// new 
#[derive(WorldQueryData)]
#[world_query_data(mutable, derive(Debug))]
struct CustomQuery {
    entity: Entity,
    a: &'static mut ComponentA,
}

#[derive(WorldQueryFilter)]
struct QueryFilter {
    _c: With<ComponentC>
}
```
- Replace `Option<With<T>>` with `Has<T>`

```rust
/// old
fn my_system(query: Query<(Entity, Option<With<ComponentA>>)>)
{
  for (entity, has_a_option) in query.iter(){
    let has_a:bool = has_a_option.is_some();
    //todo!()
  }
}

/// new
fn my_system(query: Query<(Entity, Has<ComponentA>)>)
{
  for (entity, has_a) in query.iter(){
    //todo!()
  }
}
```

- Fix queries which had filters in the data position or vice versa.

```rust
// old
fn my_system(query: Query<(Entity, With<ComponentA>)>)
{
  for (entity, _) in query.iter(){
  //todo!()
  }
}

// new
fn my_system(query: Query<Entity, With<ComponentA>>)
{
  for entity in query.iter(){
  //todo!()
  }
}

// old
fn my_system(query: Query<AnyOf<(&ComponentA, With<ComponentB>)>>)
{
  for (entity, _) in query.iter(){
  //todo!()
  }
}

// new
fn my_system(query: Query<Option<&ComponentA>, Or<(With<ComponentA>, With<ComponentB>)>>)
{
  for entity in query.iter(){
  //todo!()
  }
}

```

---------

Co-authored-by: Alice Cecile <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use D-Complex Quite challenging from either a design or technical perspective. Ask for help!
Projects
None yet
4 participants