-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Rename EntityBorrow/TrustedEntityBorrow to ContainsEntity/EntityEquivalent #18470
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
Rename EntityBorrow/TrustedEntityBorrow to ContainsEntity/EntityEquivalent #18470
Conversation
9e5b9b8
to
8dcb6f1
Compare
@@ -1117,7 +1117,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator<Item = Entity>> Debug | |||
/// Entities that don't match the query are skipped. | |||
/// | |||
/// This struct is created by the [`Query::iter_many`](crate::system::Query::iter_many) and [`Query::iter_many_mut`](crate::system::Query::iter_many_mut) methods. | |||
pub struct QueryManyIter<'w, 's, D: QueryData, F: QueryFilter, I: Iterator<Item: EntityBorrow>> { | |||
pub struct QueryManyIter<'w, 's, D: QueryData, F: QueryFilter, I: Iterator<Item: EntityEquivalent>> |
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.
I didn't see this called out in the description (hard to tell from this if the tightening here is important)
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.
I touched on it with "We should only accept EntityEquivalent
in our APIs".
This PR represent a loosening of EntityBorrow
/ContainsEntity
, which means it is now too broad/implicit to accept in our API.
By just accepting EntityEquivalent
, we have a strong guarantee that only types close to Entity
get passed to our code, and that we do not see strange behavior in entity()
calls.
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 wasn't expecting that change, either. It seems reasonable! It's just hard to notice amid the similar non-functional changes. It might help to mention explicitly in the first few lines of the PR description that you're changing the bound on Query::iter_many
and friends.
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.
Fair enough, I'll add this to the PR description!
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.
Looks good! EntityEquivalent
does seem more clear than TrustedEntityBorrow
, especially as there is no longer any borrowing involved!
/// | ||
/// The above equivalence must also hold through and between calls to any [`Clone`] and | ||
/// [`Borrow`]/[`BorrowMut`] impls in place of [`entity()`]. This is also required of any [`Hash`] impl, | ||
/// but only when [`Hash::hash`] is called with a deterministic [`Hasher`]. |
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.
I'd consider a deterministic Hash
to be one that makes a deterministic sequence of calls to the provided Hasher
. The fact that the Hasher
can do non-deterministic things doesn't change the fact that the Hash
was deterministic.
It might be simpler to say that Hash::hash
must be implemented as self.entity().hash(state);
, since there isn't really any reason to implement it any other way.
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 similar to the argument I made in #18408 but I'll describe my current thoughts here:
I think I have to rewrite the Hash
section again, because the "total order equivalence" is something this trait or any specific implementation of the Hash
trait just cannot guarantee. The only reason why we are able to use HashSet
s/HashMap
s is that they have recovery logic in case of collisions, they don't need to receive a total order of hashes to produce a "set". All they need is deterministic hashes, deterministic Eq
impls, and matching Borrow
.
A deterministic Hash
impl and a deterministically obtained hash value are not the same, and the latter is also something this trait cannot guarantee.
Because a Hash
impl is required by the trait definition to be generic over all H: Hasher
, it can only be deterministic for all H
if it does not use H
at all.
Because determinism cannot be guaranteed for all H
, we need to limit the guarantee to those H
for which it can!
All we can do is to then explicitly state that for a final hash to be unique, there need to be additional, separate guarantees that H
is deterministic for all of its trait methods, and that the Hasher/Hash
combination be collisionless.
If the combination is not collisionless, an Eq
fallback is required, like what HashSet
/HashMap
do.
With such a description, both us library authors and end users can determine when a HashSet
can be relied on as a "set" by unsafe code, regardless of what the actual hash set type is. Whether that might be hashbrown::HashSet
, indexmap::IndexSet
, our wrappers, or something else entirely.
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.
I changed the requirement to "must delegate Hash
". And added an explanation concerning its use!
What's the purpose of the If we aren't going to use it as a bound in
Your plan here is |
Not being a bound doesn't mean that it isn't useful! The intent is that an It is a common pattern for types that have an
Yeah! |
Please bother me when this is finished and I'll merge it for you :) |
@alice-i-cecile Finished! |
…alent (#18470) # Objective Fixes #9367. Yet another follow-up to #16547. These traits were initially based on `Borrow<Entity>` because that trait was what they were replacing, and it felt close enough in meaning. However, they ultimately don't quite match: `borrow` always returns references, whereas `EntityBorrow` always returns a plain `Entity`. Additionally, `EntityBorrow` can imply that we are borrowing an `Entity` from the ECS, which is not what it does. Due to its safety contract, `TrustedEntityBorrow` is important an important and widely used trait for `EntitySet` functionality. In contrast, the safe `EntityBorrow` does not see much use, because even outside of `EntitySet`-related functionality, it is a better idea to accept `TrustedEntityBorrow` over `EntityBorrow`. Furthermore, as #9367 points out, abstracting over returning `Entity` from pointers/structs that contain it can skip some ergonomic friction. On top of that, there are aspects of #18319 and #18408 that are relevant to naming: We've run into the issue that relying on a type default can switch generic order. This is livable in some contexts, but unacceptable in others. To remedy that, we'd need to switch to a type alias approach: The "defaulted" `Entity` case becomes a `UniqueEntity*`/`Entity*Map`/`Entity*Set` alias, and the base type receives a more general name. `TrustedEntityBorrow` does not mesh clearly with sensible base type names. ## Solution Replace any `EntityBorrow` bounds with `TrustedEntityBorrow`. + Rename them as such: `EntityBorrow` -> `ContainsEntity` `TrustedEntityBorrow` -> `EntityEquivalent` For `EntityBorrow` we produce a change in meaning; We designate it for types that aren't necessarily strict wrappers around `Entity` or some pointer to `Entity`, but rather any of the myriad of types that contain a single associated `Entity`. This pattern can already be seen in the common `entity`/`id` methods across the engine. We do not mean for `ContainsEntity` to be a trait that abstracts input API (like how `AsRef<T>` is often used, f.e.), because eliding `entity()` would be too implicit in the general case. We prefix "Contains" to match the intuition of a struct with an `Entity` field, like some contain a `length` or `capacity`. It gives the impression of structure, which avoids the implication of a relationship to the `ECS`. `HasEntity` f.e. could be interpreted as "a currently live entity", As an input trait for APIs like #9367 envisioned, `TrustedEntityBorrow` is a better fit, because it *does* restrict itself to strict wrappers and pointers. Which is why we replace any `EntityBorrow`/`ContainsEntity` bounds with `TrustedEntityBorrow`/`EntityEquivalent`. Here, the name `EntityEquivalent` is a lot closer to its actual meaning, which is "A type that is both equivalent to an `Entity`, and forms the same total order when compared". Prior art for this is the [`Equivalent`](https://docs.rs/hashbrown/latest/hashbrown/trait.Equivalent.html) trait in `hashbrown`, which utilizes both `Borrow` and `Eq` for its one blanket impl! Given that we lose the `Borrow` moniker, and `Equivalent` can carry various meanings, we expand on the safety comment of `EntityEquivalent` somewhat. That should help prevent the confusion we saw in [#18408](#18408 (comment)). The new name meshes a lot better with the type aliasing approach in #18408, by aligning with the base name `EntityEquivalentHashMap`. For a consistent scheme among all set types, we can use this scheme for the `UniqueEntity*` wrapper types as well! This allows us to undo the switched generic order that was introduced to `UniqueEntityArray` by its `Entity` default. Even without the type aliases, I think these renames are worth doing! ## Migration Guide Any use of `EntityBorrow` becomes `ContainsEntity`. Any use of `TrustedEntityBorrow` becomes `EntityEquivalent`.
Objective
Fixes #9367.
Yet another follow-up to #16547.
These traits were initially based on
Borrow<Entity>
because that trait was what they were replacing, and it felt close enough in meaning.However, they ultimately don't quite match:
borrow
always returns references, whereasEntityBorrow
always returns a plainEntity
.Additionally,
EntityBorrow
can imply that we are borrowing anEntity
from the ECS, which is not what it does.Due to its safety contract,
TrustedEntityBorrow
is important an important and widely used trait forEntitySet
functionality.In contrast, the safe
EntityBorrow
does not see much use, because even outside ofEntitySet
-related functionality, it is a better idea to acceptTrustedEntityBorrow
overEntityBorrow
.Furthermore, as #9367 points out, abstracting over returning
Entity
from pointers/structs that contain it can skip some ergonomic friction.On top of that, there are aspects of #18319 and #18408 that are relevant to naming:
We've run into the issue that relying on a type default can switch generic order. This is livable in some contexts, but unacceptable in others.
To remedy that, we'd need to switch to a type alias approach:
The "defaulted"
Entity
case becomes aUniqueEntity*
/Entity*Map
/Entity*Set
alias, and the base type receives a more general name.TrustedEntityBorrow
does not mesh clearly with sensible base type names.Solution
Replace any
EntityBorrow
bounds withTrustedEntityBorrow
.+
Rename them as such:
EntityBorrow
->ContainsEntity
TrustedEntityBorrow
->EntityEquivalent
For
EntityBorrow
we produce a change in meaning; We designate it for types that aren't necessarily strict wrappers aroundEntity
or some pointer toEntity
, but rather any of the myriad of types that contain a single associatedEntity
.This pattern can already be seen in the common
entity
/id
methods across the engine.We do not mean for
ContainsEntity
to be a trait that abstracts input API (like howAsRef<T>
is often used, f.e.), because elidingentity()
would be too implicit in the general case.We prefix "Contains" to match the intuition of a struct with an
Entity
field, like some contain alength
orcapacity
.It gives the impression of structure, which avoids the implication of a relationship to the
ECS
.HasEntity
f.e. could be interpreted as "a currently live entity",As an input trait for APIs like #9367 envisioned,
TrustedEntityBorrow
is a better fit, because it does restrict itself to strict wrappers and pointers. Which is why we replace anyEntityBorrow
/ContainsEntity
bounds withTrustedEntityBorrow
/EntityEquivalent
.Here, the name
EntityEquivalent
is a lot closer to its actual meaning, which is "A type that is both equivalent to anEntity
, and forms the same total order when compared".Prior art for this is the
Equivalent
trait inhashbrown
, which utilizes bothBorrow
andEq
for its one blanket impl!Given that we lose the
Borrow
moniker, andEquivalent
can carry various meanings, we expand on the safety comment ofEntityEquivalent
somewhat. That should help prevent the confusion we saw in #18408.The new name meshes a lot better with the type aliasing approach in #18408, by aligning with the base name
EntityEquivalentHashMap
.For a consistent scheme among all set types, we can use this scheme for the
UniqueEntity*
wrapper types as well!This allows us to undo the switched generic order that was introduced to
UniqueEntityArray
by itsEntity
default.Even without the type aliases, I think these renames are worth doing!
Migration Guide
Any use of
EntityBorrow
becomesContainsEntity
.Any use of
TrustedEntityBorrow
becomesEntityEquivalent
.