Add 'with' and 'without' methods to EngineBitfield.#1627
Conversation
|
I am aware this needs some adjustments
However wanted to at least go ahead and open this for awareness. I have a use case where I want to duplicate nodes without using instantiation, but to me the most "proper" way is to use "default" flags (without hardcoding them) and erase only the flag I don't want (USE_INSTANTIATION) with bitwise |
|
API docs are being generated and will be shortly available at: https://godot-rust.github.io/docs/gdext/pr-1627 |
|
Thanks for your contribution!
This is a fair point, and the problem is that it's hard to define a default generally. In this case it's somewhat evident from the name Here are all bitfields in Godot, some observations:
So, I believe the responsible thing to do here would be to not ship an automatic However, removing
The PR currently lacks tests -- and those would likely highlight some of the problems with
I agree that DuplicateFlags::default() & !DuplicateFlags::USE_INSTANTIATION
DuplicateFlags::default().unset(DuplicateFlags::USE_INSTANTIATION)So the question is, do we have other use cases for Another thing I considered was a method So this definitely needs some thought. PR-wise, please also squash your commits on next update according to Contribution Guidelines. |
|
Thanks for taking a careful look at this!
For my use case a named method would be entirely sufficient, I have no
specific need for the bitwise operators themselves at the game's level.
As far as defaults, an alternative -- considering the specific flags I'm
generally wanting defaults for are very specifically used for particular
functions (i.e. `DuplicateFlags` is for its one niche use case for passing to
`Node::duplicate_node_ex().flags(...)` ) -- would perhaps be to add simply
a getter in addition to the `flags` setter. Granted it might be a bit more
verbose, i.e. for this, I would think to write as:
```
let duplicated = {
let mut duplication = original_node.duplicate_node_ex();
let mut flags = duplication.get_flags();
flags.unset(DuplicateFlags::USE_INSTANTIATION); // if this hypothetical
method returns the modified flags, wouldn't be necessary to store flags in
variable.
duplication.flags(flags).done()
};
```
Perhaps instead of (or in addition to, but I understand not wanting to
unnecessarily proliferate the amount of ways to do the same thing for
future maintenance reasons and test coverage) adding these capabilities to
the flags, the builder returned by `Node::duplicate_node_ex` might directly
have `unset_flag` and `set_flag` itself, which would make this the most
compact. If `BitOr` weren't already "the" way to combine flags, this would
cover all use cases if there were a `clear_flags` method on this builder
too.
```
let duplicated =
original_node.duplicate_node_ex().unset_all_flags().set_flag(the_only_flag_you_want).done();
```
The downside with taking power away from `DuplicateFlags` is also with
storing some flags, especially as a convenient `const` if one is frequently
using particular flags. It would be clunky to represent that. i.e. `const
MY_DUPE_FLAGS: DuplicateFlags = flag1 | flag2;` versus
```
fn apply_my_dupe_flags (mut builder: DuplicateBuilder) - {
builder.set_flag(flag1);
builder.set_flag(flag2);
builder
}
```
A consideration is consistency with existing pattern. If "the" way to work
with `DuplicateFlags` so far is the bitwise operators (`|`), using methods in
other cases (`&`, `!`) is potentially going to just feel strange as a user of
the API. "Do they want me to use bitwise operations or not?"
Ultimately I will be glad to throw together whichever solution is
preferable, and clean this up (including tests).
|
|
@rsethc would it be possible for you to use the GitHub web interface rather than email? I'm not sure if you saw my edits, plus your message doesn't seem to apply Markdown formatting, and has overly short line lengths, making it harder to read. Changing I agree that mixing Maybe let new_value = bitfield.with(MyBitfield::A).without(MyBitfield::B);versus: let new_value = (bitfield | MyBitfield::A) & !MyBitfield::B; |
|
Thanks, I'm seeing the
|
c5ee9ec to
3aed515
Compare
3aed515 to
35a0d9a
Compare
| // Test that when adding a flag, where some are | ||
| // already present, the ones that are supposed to | ||
| // be added do in fact get added. | ||
| assert_eq!( | ||
| DuplicateFlags::GROUPS.with( | ||
| DuplicateFlags::GROUPS.with(DuplicateFlags::SIGNALS) | ||
| ), | ||
| DuplicateFlags::GROUPS.with(DuplicateFlags::SIGNALS), | ||
| ); |
There was a problem hiding this comment.
I'm not sure if this is necessary -- it's kinda already tested by adding GROUPS with itself, and adding GROUPS with SIGNALS.
A more interesting case might be chaining two with calls for separate flags.
I'd also remove the comments above all assert_eq! statements; the asserts are self-explanatory. The exception is if you really want to test something specific/unexpected. But then you can also use the 3rd argument of assert_eq!.
There was a problem hiding this comment.
The point of it is mainly to detect any (unlikely) weird implementation someone might replace the bitwise flags with in the future that toggles flags without checking they were already enabled. Or something else bizarre like that. Might as well.
In the spirit of might-as-well I will also add what you've mentioned below this, for both (i.e. adding with chaining test, without chaining test).
There was a problem hiding this comment.
Added that test without removing the one the comment is about. Please resolve or reply depending on agree/disagree, thanks.
|
Thanks for the updates already! Btw, don't spend time on commit messages like:
Eventually this should be 1 commit, so everything else is going to be thrown away. Feel free to always squash and force-push, I'm only looking at the complete diff anyway 🙂 |
|
Such commit messages are for the un-squashed branches in my own fork for my own notes, even though these will be lost here due to squashing in this particular branch for this PR. |
ebe0c98 to
370ac19
Compare
370ac19 to
27d90ca
Compare
27d90ca to
8ebf527
Compare
499dfba to
8709aff
Compare
|
|
||
| /// Necessarily since `from_ord` is not `const`. | ||
| fn no_flags() -> DuplicateFlags { | ||
| // Could use `Default::default()` here. Until it is broken by `!1630`, funnily enough. |
There was a problem hiding this comment.
| // Could use `Default::default()` here. Until it is broken by `!1630`, funnily enough. | |
| // Avoid `Default::default()` as its value and presence might change in a future version. |
There was a problem hiding this comment.
But actually I think there's not much point in having this additional indirection, just use from_ord(0) inline -- see below.
| const SIGNALS: DuplicateFlags = DuplicateFlags::SIGNALS; | ||
| const GROUPS: DuplicateFlags = DuplicateFlags::GROUPS; | ||
| const USE_INSTANTIATION: DuplicateFlags = DuplicateFlags::USE_INSTANTIATION; | ||
|
|
||
| #[itest] | ||
| fn bitfield_ops_with() { | ||
| let no_flags = no_flags(); | ||
|
|
||
| assert_eq!(no_flags.with(USE_INSTANTIATION), USE_INSTANTIATION); | ||
|
|
||
| assert_eq!( | ||
| GROUPS.with(SIGNALS), | ||
| DuplicateFlags::from_ord(GROUPS.ord() | SIGNALS.ord()) | ||
| ); | ||
|
|
||
| assert_eq!(GROUPS.with(GROUPS), GROUPS); | ||
|
|
||
| assert_eq!(GROUPS.with(GROUPS.with(SIGNALS)), GROUPS.with(SIGNALS)); | ||
|
|
||
| let with_then_with = GROUPS.with(SIGNALS).with(USE_INSTANTIATION); | ||
| assert!(with_then_with.is_set(GROUPS)); | ||
| assert!(with_then_with.is_set(SIGNALS)); | ||
| assert!(with_then_with.is_set(USE_INSTANTIATION)); | ||
|
|
||
| assert_eq!(GROUPS.with(no_flags), GROUPS); | ||
| } |
There was a problem hiding this comment.
The test pattern is inconsistent. In some cases you verify based on the numeric values, other cases with is_set (which is strictly weaker because it doesn't account for other bits being set).
Also, you can just use SCRIPTS instead of USE_INSTANTIATION so it's easier to read. And we can even go one step further to check against the numeric values -- they must be stable for compatibility.
Then next thing, you can group all operations yielding the same result, there's no point doing NO_FLAGS.with(x) check at the start and y.with(NO_FLAGS) at the end, and x being different than y 😉
It's a bit nitpicky, but I think it really helps comprehension. Here's a more compact version. I even added two checks for commutativity.
| const SIGNALS: DuplicateFlags = DuplicateFlags::SIGNALS; | |
| const GROUPS: DuplicateFlags = DuplicateFlags::GROUPS; | |
| const USE_INSTANTIATION: DuplicateFlags = DuplicateFlags::USE_INSTANTIATION; | |
| #[itest] | |
| fn bitfield_ops_with() { | |
| let no_flags = no_flags(); | |
| assert_eq!(no_flags.with(USE_INSTANTIATION), USE_INSTANTIATION); | |
| assert_eq!( | |
| GROUPS.with(SIGNALS), | |
| DuplicateFlags::from_ord(GROUPS.ord() | SIGNALS.ord()) | |
| ); | |
| assert_eq!(GROUPS.with(GROUPS), GROUPS); | |
| assert_eq!(GROUPS.with(GROUPS.with(SIGNALS)), GROUPS.with(SIGNALS)); | |
| let with_then_with = GROUPS.with(SIGNALS).with(USE_INSTANTIATION); | |
| assert!(with_then_with.is_set(GROUPS)); | |
| assert!(with_then_with.is_set(SIGNALS)); | |
| assert!(with_then_with.is_set(USE_INSTANTIATION)); | |
| assert_eq!(GROUPS.with(no_flags), GROUPS); | |
| } | |
| const SIGNALS: DuplicateFlags = DuplicateFlags::SIGNALS; // 1 | |
| const GROUPS: DuplicateFlags = DuplicateFlags::GROUPS; // 2 | |
| const SCRIPTS: DuplicateFlags = DuplicateFlags::SCRIPTS; // 4 | |
| #[itest] | |
| fn bitfield_ops_with() { | |
| let no_flags = DuplicateFlags::from_ord(0); | |
| assert_eq!(no_flags.with(GROUPS).ord(), 2); | |
| assert_eq!(GROUPS.with(no_flags).ord(), 2); | |
| assert_eq!(GROUPS.with(GROUPS).ord(), 2); | |
| assert_eq!(GROUPS.with(SIGNALS).ord(), 1 | 2); | |
| assert_eq!(GROUPS.with(GROUPS.with(SIGNALS)).ord(), 1 | 2); | |
| assert_eq!(GROUPS.with(GROUPS).with(SIGNALS).ord(), 1 | 2); | |
| assert_eq!(GROUPS.with(SIGNALS).with(SCRIPTS).ord(), 1 | 2 | 4); | |
| } |
| #[itest] | ||
| fn bitfield_ops_without() { | ||
| let no_flags = no_flags(); | ||
|
|
||
| assert_eq!(USE_INSTANTIATION.without(USE_INSTANTIATION), no_flags); | ||
|
|
||
| assert_eq!(SIGNALS.with(GROUPS).without(SIGNALS), GROUPS); | ||
|
|
||
| assert_eq!(GROUPS.without(SIGNALS), GROUPS); | ||
|
|
||
| assert_eq!( | ||
| SIGNALS | ||
| .with(GROUPS) | ||
| .without(SIGNALS.with(USE_INSTANTIATION)), | ||
| GROUPS | ||
| ); | ||
|
|
||
| let without_then_without = GROUPS | ||
| .with(SIGNALS) | ||
| .with(USE_INSTANTIATION) | ||
| .without(SIGNALS) | ||
| .without(USE_INSTANTIATION); | ||
| assert!(without_then_without.is_set(GROUPS)); | ||
| assert!(!without_then_without.is_set(SIGNALS)); | ||
| assert!(!without_then_without.is_set(USE_INSTANTIATION)); | ||
|
|
||
| assert_eq!(GROUPS.without(no_flags), GROUPS); | ||
| } |
There was a problem hiding this comment.
| #[itest] | |
| fn bitfield_ops_without() { | |
| let no_flags = no_flags(); | |
| assert_eq!(USE_INSTANTIATION.without(USE_INSTANTIATION), no_flags); | |
| assert_eq!(SIGNALS.with(GROUPS).without(SIGNALS), GROUPS); | |
| assert_eq!(GROUPS.without(SIGNALS), GROUPS); | |
| assert_eq!( | |
| SIGNALS | |
| .with(GROUPS) | |
| .without(SIGNALS.with(USE_INSTANTIATION)), | |
| GROUPS | |
| ); | |
| let without_then_without = GROUPS | |
| .with(SIGNALS) | |
| .with(USE_INSTANTIATION) | |
| .without(SIGNALS) | |
| .without(USE_INSTANTIATION); | |
| assert!(without_then_without.is_set(GROUPS)); | |
| assert!(!without_then_without.is_set(SIGNALS)); | |
| assert!(!without_then_without.is_set(USE_INSTANTIATION)); | |
| assert_eq!(GROUPS.without(no_flags), GROUPS); | |
| } | |
| #[itest] | |
| fn bitfield_ops_without() { | |
| let no_flags = DuplicateFlags::from_ord(0); | |
| assert_eq!(no_flags.without(no_flags).ord(), 0); | |
| assert_eq!(GROUPS.without(GROUPS).ord(), 0); | |
| assert_eq!(GROUPS.without(SIGNALS).ord(), 2); | |
| assert_eq!(SIGNALS.with(GROUPS).without(SIGNALS).ord(), 2); | |
| assert_eq!(SIGNALS.with(GROUPS).without(SIGNALS.with(SCRIPTS)).ord(), 2); | |
| let all = GROUPS.with(SIGNALS).with(SCRIPTS); | |
| assert_eq!(all.without(SIGNALS).ord(), 2 | 4); | |
| assert_eq!(all.without(SIGNALS).without(SCRIPTS).ord(), 2); | |
| } |
let flags_with_one_removed = more_flags & !excluded_flagpattern).Defaultimplementation function body, for when the#[derive(Default)]default value would not make sense. In this case, theDuplicateFlagswhere theDefault::default()will now evaluate to the same default that is set byNode::duplicate_nodeand initially byNode::duplicate_node_ex.