Skip to content

Shortcut for With and Without #9255

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

tguichaoua
Copy link
Contributor

@tguichaoua tguichaoua commented Jul 23, 2023

Objective

This PR implements a proposition to make With<(A, B, C)> and Without<(A, B, C)> a shortcut for (With<A>, With<B>, With<C>) and (Without<A>, Without<B>, Without<C>) to help reduce verbosity of queries.

With<Bundle> or Without<Bundle> are still disallowed as it's considered an anti-pattern.

Solution

Implements WorldQuery and ReadOnlyWorldQuery for Filter<(T0, T1, ...)> that delegate to <(Fitler<T0>, Filter<T1>, ...) as WorldQuery>, where Filter is With or Without.


Changelog

  • Query filters like (With<A>, With<B>, With<C>) and (Without<A>, Without<B>, Without<C>) can be simplified into With<(A, B, C)> and Without<(A, B, C)> respectively.

@nicopap nicopap added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use labels Jul 23, 2023
@alice-i-cecile alice-i-cecile added the X-Controversial There is active debate or serious implications around merging this PR label Jul 24, 2023
@Selene-Amanita Selene-Amanita added this to the 0.12 milestone Jul 24, 2023
Copy link
Member

@joseph-gio joseph-gio left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks good to me.

This is a welcome addition -- I've always found it surprising that this didn't work already, it just feels natural to be able to filter multiple components at once like this.

Copy link
Member

@james7132 james7132 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR implements a proposition to make With<(A, B, C)> and Without<(A, B, C)> a shortcut for (With<A>, With<B>, With<C>) and (Without<A>, Without<B>, Without<C>) to help reduce verbosity of queries.

From my perspective, this would introduce inconsistent behavior in the Without case. In both the normal query and With cases, (A, B, C) means A AND B AND C, while Without<(A, B, C)> translates to NOT A AND NOT B AND NOT C instead of NOT (A AND B AND C). Applying De Morgan's law, Without<(A, B, C)> actually translates to Or<(Without<A>, Without<B>, Without<C>)>.


/// This macro implements `WorldQuery` and `ReadOnlyWorldQuery` for a filter
/// so that `Filter<(A, B, C)>` acts as a shortcut for `(Filter<A>, Filter<B>, Filter<C>)`.
macro_rules! impl_filter_tuple_to_tuple_filter {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This macro and the two invocations adds yet another set of tuple pyramids, which can add a lot of additional code to compile. Can you check the difference in compile time for bevy_ecs before and after this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the changes, bevy_ecs took about half a second longer to compile.

rustc 1.71.0 (8ede3aae2 2023-07-12)
Host: x86_64-pc-windows-msvc
Target: x86_64-pc-windows-msvc

@tguichaoua
Copy link
Contributor Author

tguichaoua commented Jul 28, 2023

This PR implements a proposition to make With<(A, B, C)> and Without<(A, B, C)> a shortcut for (With<A>, With<B>, With<C>) and (Without<A>, Without<B>, Without<C>) to help reduce verbosity of queries.

From my perspective, this would introduce inconsistent behavior in the Without case. In both the normal query and With cases, (A, B, C) means A AND B AND C, while Without<(A, B, C)> translates to NOT A AND NOT B AND NOT C instead of NOT (A AND B AND C). Applying De Morgan's law, Without<(A, B, C)> actually translates to Or<(Without<A>, Without<B>, Without<C>)>.

In terms of logic and since Without<T> is defined as Not<With<T>>, you're right.

But if we break this relation between Without<T> and With<T>, and we consider them as two different filters, we gains:

  • consistency : for every Filter<T>, Filter<(A, B, C)> is a shortcut for (Filter<A>, Filter<B>, Filter<C>);
  • ergonomy :
    • With<(A, B, C)> means "It contains all of A, B and C"
    • Without<(A, B, C)> means "It contains none of A, B or C"1 (which is expressed as (Without<A>, Without<B>, Without<C>))

The only thing lost is that With<T> and Without<T> are complementary.
Are there use cases where this complementarity is used?

Without implementation comparison
let mut world = World::new();

let e1 = world.spawn(A(0)).id();
let e2 = world.spawn((A(0), B(0))).id();
let e3 = world.spawn((B(0), C(0))).id();
let e4 = world.spawn((A(0), B(0), C(0))).id();
let e5 = world.spawn((C(0), D(0))).id();

let with_a_b = world
    .query_filtered::<Entity, (With<A>, With<B>)>()
    .iter(&world)
    .collect::<HashSet<Entity>>();

let without_a_and_b = world
    .query_filtered::<Entity, (Without<A>, Without<B>)>()
    .iter(&world)
    .collect::<HashSet<Entity>>();

let without_a_or_b = world
    .query_filtered::<Entity, Or<(Without<A>, Without<B>)>>()
    .iter(&world)
    .collect::<HashSet<Entity>>();

assert_eq!(with_a_b, HashSet::from([e2, e4]));            // entities with both `A` and `B`
assert_eq!(without_a_and_b, HashSet::from([e5]));         // entities with none of `A` nor `B`
assert_eq!(without_a_or_b, HashSet::from([e1, e3, e5])); // entities that are not in `with_a_b` (complementarity)

Footnotes

  1. this is just a personal feeling on how to translate Without<(A, B, C)> in plain text, any feedback is welcome.

@tguichaoua
Copy link
Contributor Author

tguichaoua commented Jul 28, 2023

I think CI is broken, it fails with the message

 method `clone_fetch` is not a member of trait `WorldQuery`

My last commit update the test function only. There is no reason for my previous commit to pass but not this one.

Also, I can build locally.

@Selene-Amanita
Copy link
Member

Selene-Amanita commented Aug 3, 2023

I think it could be both argued that:

  1. Without<(A,B)> would stand for Without<A>, Without<B> (current behavior of the PR), because you have two without in the filter tuple, you simply want to combine them into one, and that's the most common use case
  2. Without<(A,B)> would stand for Or<(Without<A>, Without<B>)> because that the logical way of applying not(A and B), so that's what you would expect if you think about the coma as an "and", and also it would make more sense to simplify this notation rather than the notation in 1. because it's longer.

I see three options:

  • we just allow to put a tuple in With, not Without
  • we choose 1. or 2., I think personally it would make more sense to take 1.
  • we separate without into:
    • Without that can not be used with a tuple inside
    • WithoutAny with 2. behavior
    • WithoutAll with 1. behavior (or WithNone)

With the third option we could also similarly have WithAny and WithAll. All of those could also be named:

  • Any for WithAny
  • All for WithAll
  • None for WithoutAll
  • NotAll for WithoutAny

I would be more in favor of this last option, and this last naming convention

@iiYese
Copy link
Contributor

iiYese commented Aug 3, 2023

Applying De Morgan's law, Without<(A, B, C)> actually translates to Or<(Without<A>, Without<B>, Without<C>)>.

I don't see this as being the case. IMO With and Without with this change should be viewed as predicate functions that take a variable number of arguments. Ie. this PR has the most correct, expected and consistent behavior. Boolean algebra isn't applicable here.

This idea of using tuples to create var args is also in line with how it's used everywhere else in bevy.

EDIT:
After thinking about it more the problem here is that there aren't distinct mechanisms for expressing var args and logical AND which is unique to filters because () is implicitly AND. The question here is not "is this correct or is this correct". The question is does this mean var args? Or does this mean AND?

Making () be AND was a mistake and this discussion goes away in the presence of an explicit And<(...)> combinator. In Queries, Bundles, SystemsConfigs, etc. tuples just mean multiple items and do not have special meaning.

@maniwani
Copy link
Contributor

maniwani commented Aug 4, 2023

Applying De Morgan's law, Without<(A, B, C)> actually translates to Or<(Without<A>, Without<B>, Without<C>)>.

Yeah, I first thought Without<(A, B, C)> meant "without any of A, B, C".

IMO With and Without with this change should be viewed as predicate functions that take a variable number of arguments. i.e. this PR has the most correct, expected and consistent behavior. Boolean algebra isn't applicable here.

It does apply. This shortcut to reduce "boilerplate" breaks predictable assumptions some users will make. We could decide to stop trying to apply boolean algebra if we think this shortcut is useful enough to outweigh that, but we still need to decide that first.

If this change goes through, subtle logic bugs that were not possible before will be. I think lots of users will confuse Without<(A, B)> and Or(Without<A>, Without<B>) even if we boldly highlight it in the docs.

(edit: I'm not sure how I feel about this. I think I'd prefer not making this change, but I don't have a strong opinion on it.)

Making tuples mean AND was a mistake.

🤔 I could get behind an explicit And. It feels weird to have Or but not And.

@B-Reif
Copy link
Contributor

B-Reif commented Aug 4, 2023

I’m in favor of deprecating the use of implicit-AND tuples and moving toward explicit And (as in #9347).

@iiYese
Copy link
Contributor

iiYese commented Aug 5, 2023

Another option:

  • Add Any<Bundle, Pred> + All<Bundle, Pred> APIs.
    // Has all
    All<(A, B, C), With>
    // Does not have atleast one of
    Any<(A, B, C), Without>
    // At least one of these were changed
    Any<(A, B, C), Changed>
  • Keep implicit AND.
    ECS is very like prolog and I think leaning into that would be best. This is already the case and will become even more obvious when we add entity relations. It also makes for the most universal way to reason about everything mentioned in Make logical AND explicit in filters with an And<(..)> combinator #9347.

@alice-i-cecile alice-i-cecile removed this from the 0.12 milestone Sep 30, 2023
@janhohenheim
Copy link
Member

Note from the future: variable tuple args mean OR in triggers now.

@BenjaminBrienen
Copy link
Contributor

Mentioned issue is closed, 2 years old/stale PR, merge conflicts

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-Feature A new feature, making something new possible C-Usability A targeted quality-of-life change that makes Bevy easier to use S-Nominated-To-Close A triage team member thinks this PR or issue should be closed out. X-Controversial There is active debate or serious implications around merging this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support With<Bundle> rather than just With<Component>