-
Notifications
You must be signed in to change notification settings - Fork 66
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
Reflexive world queries #68
Open
JoJoJet
wants to merge
26
commits into
bevyengine:main
Choose a base branch
from
JoJoJet:reflexive-world-queries
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
bd353a5
Create reflexive-world-queries.md
JoJoJet f23f025
add future possibilities
JoJoJet 0def33b
add a user-facing explanation
JoJoJet 7243490
add a minimal unresolved questions section
JoJoJet dcbf1a8
remove some things
JoJoJet c402362
note that most types are reflexive
JoJoJet 496c810
collapse a paragraph
JoJoJet 6083c1a
fix an article
JoJoJet 7e9a60a
compare to system params
JoJoJet 2b0d37b
put the RFC number in the filename
JoJoJet 4449c68
make an example pub
JoJoJet e7780b4
indent a definition
JoJoJet 5cb49fb
accidentally a word
JoJoJet b34d4c9
add an example for `AnyOf`
JoJoJet 1a97203
`entity` -> `id`
JoJoJet 534dba0
complain about more things
JoJoJet d7370df
emphasize a clause
JoJoJet 0f51714
describe derived types more precisely
JoJoJet f570b1d
remove an example
JoJoJet 82a5ee5
condense an example
JoJoJet 7d29f57
lighten
JoJoJet 98bddef
fix RFC number
JoJoJet 425dc78
Apply suggestions from code review
JoJoJet 5fcf590
humble bevy user
JoJoJet ed2b8db
i can't type
JoJoJet 6ca873d
add another example of reflexive primitive types
JoJoJet File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
# Feature Name: `reflexive-world-queries` | ||
|
||
## Summary | ||
|
||
Types created using `#[derive(WorldQuery)]` are now reflexive, | ||
meaning we no longer generate 'Item' structs for each custom WorldQuery. | ||
This makes the API for these types much simpler. | ||
|
||
## Motivation | ||
|
||
You, a humble Bevy user, wish to define your own custom `WorldQuery` type. | ||
It should display the name of an entity if it has one, and fall back | ||
to showing the entity's ID if it doesn't. | ||
|
||
```rust | ||
#[derive(WorldQuery)] | ||
pub struct DebugName { | ||
pub name: Option<&'static Name>, | ||
pub id: Entity, | ||
} | ||
|
||
impl Debug for DebugName { | ||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt:Result { | ||
if let Some(name) = self.name { | ||
write!(f, "{name}") | ||
} else { | ||
write!(f, "Entity {:?}", self.id) | ||
} | ||
} | ||
} | ||
``` | ||
|
||
This type is easy define, and should be very useful. | ||
However, you notice a strange error when trying to use it: | ||
|
||
```rust | ||
fn my_system(q: Query<DebugName>) { | ||
for dbg in &q { | ||
println!("{dbg:?}"); | ||
} | ||
} | ||
``` | ||
|
||
``` | ||
error[E0277]: `DebugNameItem<'_>` doesn't implement `Debug` | ||
--> crates\bevy_core\src\name.rs:194:20 | ||
| | ||
194 | println!("{dbg:?}"); | ||
| ^^^ `DebugNameItem<'_>` cannot be formatted using `{:?}` | ||
| | ||
= help: the trait `Debug` is not implemented for `DebugNameItem<'_>` | ||
= note: add `#[derive(Debug)]` to `DebugNameItem<'_>` or manually `impl Debug for DebugNameItem<'_>` | ||
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info) | ||
``` | ||
|
||
"What do you mean rustc?!" you plead as you bang your head against the wall. You just implemented `Debug`! | ||
|
||
The problem here is that the type returned from the query is *not* the type we just defined | ||
-- it's a hidden macro-generated type with near-identical fields. | ||
Here's what it looks like in the docs: | ||
|
||
```rust | ||
pub struct DebugNameItem<'__w> { | ||
pub name: <Option<&'static Name> as WorldQuery>::Item<'__w>, | ||
pub id: <Entity as WorldQuery>::Item<'__w>, | ||
} | ||
``` | ||
|
||
In order to fix the error, we need to implement `Debug` for *this* type: | ||
|
||
```rust | ||
impl Debug for DebugNameItem<'_> { ... } | ||
``` | ||
|
||
This avoids the compile error, but it results in an awkward situation where our documentation, | ||
trait impls, and methods are fragmented between two very similar types with a non-obvious distinction between them. | ||
|
||
Since the `DebugNameItem` is generated in a macro, it is very awkard to flesh out its API. | ||
Its documentation is necessarily vague since you can't write it yourself, and deriving traits | ||
requires the special syntax `#[world_query(derive(PartialEq))]`. | ||
|
||
## User-facing explanation | ||
|
||
### **Reflexive** | ||
|
||
1. (Adjective) *In reference to oneself.* | ||
|
||
Any `WorldQuery` type defined with `#[derive(WorldQuery)]` must be reflexive, | ||
meaning it returns itself when used in a query. | ||
Most `WorldQuery` types are reflexive, and may be used without consideration of this property. | ||
A notable exception is `&mut T`, which is incompatible with the derive macro. | ||
`Mut<T>` must be used instead. | ||
|
||
Examples: | ||
|
||
```rust | ||
#[derive(WorldQuery)] | ||
struct DebugName<'w> { | ||
name: Option<&'w Name>, | ||
id: Entity, | ||
} | ||
|
||
impl Debug for DebugName<'_> { ... } | ||
|
||
#[derive(WorldQuery)] | ||
#[world_query(mutable)] | ||
struct PhysicsComponents<'w> { | ||
mass: &'w Mass, | ||
transform: Mut<'w, Transform>, | ||
velocity: Mut<'w, Velocity>, | ||
} | ||
``` | ||
|
||
## Implementation strategy | ||
|
||
Changing the derive macro to be reflexive should be a trivial change | ||
-- the `SystemParam` derive already works this way, | ||
so the details will not be discussed in this RFC. | ||
|
||
To support this change, we will need to rework some of our built-in | ||
`WorldQuery` types to be reflexive. | ||
|
||
```rust | ||
fn any_system(q: Query<AnyOf<(&A, &B, &C)>>) { | ||
// Before: | ||
for (a, b, c) in &q { | ||
... | ||
} | ||
|
||
// After: | ||
for AnyOf((a, b, c)) in &q { | ||
... | ||
} | ||
} | ||
|
||
fn changed_system(q: Query<Changed<A>>) { | ||
// Before: | ||
for changed_a in &q { | ||
... | ||
} | ||
|
||
// After: | ||
for Changed(changed_a) in &q { | ||
... | ||
} | ||
} | ||
``` | ||
|
||
Since `&mut T` is not reflexive, we will have to implement `WorldQuery` for `Mut<T>`. | ||
|
||
## Drawbacks | ||
|
||
This will add slightly more friction in some cases, since | ||
types such as `&mut T` are forbidden from being used in the derive macro. | ||
However, these types can still be used in anonymous `WorldQuery`s or types | ||
such as `type MyQuery = (&'static mut A, &'static mut U)`. | ||
|
||
Since the `WorldQuery` derive is only used in cases where the user wishes | ||
to expose a public API, we should prioritize the cleanliness of that API | ||
over the ease of implementing it. | ||
|
||
## Rationale and alternatives | ||
|
||
Do nothing. The current situation is workable, but it's not ideal. | ||
Generated 'Item' structs add signifcant noise to a plugin's API, | ||
and make it more difficult to understand due to having several very similar types. | ||
|
||
## Unresolved questions | ||
|
||
None. | ||
|
||
## Future possibilities | ||
|
||
We should explore ways to make the `#[world_query(mutable)]` attribute unnecessary. |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is my biggest gripe with this approach. This sacrifices the simpler mental model for normal users and happy path and forces any mutative query to use
Mut<T>
over&mut T
, and that mapping from&T
to&mut T
is now lost.We can address this by implementing
&mut T: World Query
, and just forcibly mark everything matched as mutated, but that would make the distinction between&mut T
andMut<T>
extremely nuanced.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm nervous about that, especially because of how subtly it breaks existing code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if we're on the same page, but I want to clarify that you can still write
Query<&mut T>
and have it returnMut<T>
-- the reflexive constraint is only for namedWorldQuery
structs made using the derive macro. Very little user code will be affected in practice. I think this constraint will surprise users at first, but I also think it will make sense when they think about it, or read the docs and have it explained to them (we'll have to think of a good way of explaining this that would make it click more easily). Also, this behavior is consistent with how theSystemParam
derive macro works, which might make it easier to understand for some users.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another point I want to add: using
&mut T
in a derived WorldQuery already fails with a confusing type error, until you read the docs and see that you need the special attribute#[world_query(mutable)]
. Since we're already relying on the user to read the docs for this macro to be usable, I don't think there's much of a regression caused by requiring world queries to be reflexive.