-
-
Notifications
You must be signed in to change notification settings - Fork 160
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
Add iteration methods to Dictionary #99
Conversation
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.
Thanks a lot for this pull request! Very nice to see the API grow 🙂
Now we have:
keys_array -> Array
values_array -> Array
iter_shared -> Iter over (Variant, Variant)
iter_shared_as -> Iter over (K, V)
keys_shared -> Iter over Variant
keys_shared_as -> Iter over K
But we don't have an iterator over values. If we added that, we would have 8 methods already. This adds quite a bit of API surface, and our "shared" naming convention has the potential to make things rather unreadable 😬
I wonder if it's possible to have a generic iterator adapter over these 🤔
dict.keys_shared() -> Iter over Variant
dict.keys_shared().typed::<K>() -> Iter over K
dict.iter_shared().typed::<K, V>() -> Iter over (K, V)
...
This could potentially be reusable for arrays as well.
We could even consider .array()
or .collect::<Array>()
; not sure how much indirection it would add. But generally it's really nice to have such composable building blocks 🏛️
Also, I'd start with a panicking adapter that directly returns the elements, rather than delivering Result<...>
-- the Result
is mostly useful if you want to extract the result out of iteration and combine with ?
, and I'd guess that many people would just call try_from_variant()
in that case.
godot-core/src/builtin/dictionary.rs
Outdated
/// Returns an iterator over the key-value pairs from the `Dictionary` by reference. Instead of | ||
/// references to key-value pairs as you might expect, the iterator returns a (cheap, shallow) | ||
/// copy of each key-value pair. |
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 would explicitly mention the (Variant, Variant)
item type.
Also, "by reference" is unclear, see my comment in the other PR.
godot-core/src/builtin/dictionary.rs
Outdated
// The function returns a bool | ||
assert!(success == 0 || success == 1); | ||
let success = success != 0; | ||
|
||
if success { | ||
assert!(valid); | ||
Some(variant) | ||
} else { | ||
None | ||
} |
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 name the first variable is_valid
rather than success
, closer the parameter name in the header.
And then you can write this as:
match is_valid {
0 => None,
1 => Some(variant),
_ => panic!("Invalid valid status {is_valid}"),
}
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.
that would make it misleading, as valid is a different boolean from success. valid represents whether the variant sent in is valid or not, whereas the return is whether the function succeeded at returning an iterator
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 think it's what they use in the header though?
Success is fine for me too, important is the match
😉
itest/rust/src/dictionary_test.rs
Outdated
// The below tests, test to ensure no panicking happens when mutating | ||
// a dictionary while iterating. Though the result of doing is not | ||
// guaranteed. |
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.
Though the result of doing is not guaranteed.
Probably you mean that the tests passing is no guarantee that mutating while iterating is safe?
I agree that we can't be 100% certain, but I still think we can make the tests more robust. At the moment they don't contain any assertion, so which operation do you expect to panic? If you can't find a deliberately wrong implementation that would make the test fail, it's not a good test.
Even if iteration order can be random (which I think was changed to be deterministic), can we not try to evaluate at least some properties of the result?
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 can think of many wrong implementations that would make these tests fail, mainly with accidentally dereferencing invalid variants/null pointers. but im not sure there are many good properties i could test of these would be meaningful except for "it didn't panic or do any UB while i performed the operations". most relevant properties would cause these functions to panic anyway before i get a chance to test for them.
The only thing i can think of right now would be testing that the dictionaries are valid after the iteration? which i can certainly do, but it doesn't really test the core of what i want these tests to test. though i should test that either way, because i dont think i have tested that anywhere.
As of now i can't see anything in the dictionary docs guaranteeing anything about the state of a dictionary after you mutate it mid-iteration, so i'm not sure if we should rely on an undocumented implementation detail to verify the correctness of our implementation.
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.
but im not sure there are many good properties i could test of these would be meaningful except for "it didn't panic or do any UB while i performed the operations"
But panic is defined Rust behavior, and Godot methods will never do it. So what can panic? Dereferencing null pointers doesn't.
And how do you test for UB? We can run Godot compiled with address sanitizers, but at the moment we simply won't see it, except by pure chance.
What we can test is that the data structures are in a valid (even if unexpected) state. You're right that Godot doesn't specify behavior, but there's probably something we can assume. I'd rather have a test that breaks next year when they change implementation, than no test at all 🙂
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.
But panic is defined Rust behavior, and Godot methods will never do it. So what can panic? Dereferencing null pointers doesn't.
I have some assertions in code like checking various invariants that could panic if the c++ bindings don't work the way i expect them to, and if those change behavior in the future then they would panic.
And how do you test for UB? We can run Godot compiled with address sanitizers, but at the moment we simply won't see it, except by pure chance.
That was the point with what i said.
What we can test (...)
As i mentioned i will add some tests to ensure that the dictionary doesn't get messed up by the iteration with mutation. But if you don't mind relying on unspecified behavior of iteration then i can also add some tests to confirm that our iteration implementation has the same behavior as the current version of Godot, which should be true as they use the same method internally.
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.
But if you don't mind relying on unspecified behavior of iteration then i can also add some tests to confirm that our iteration implementation has the same behavior as the current version of Godot, which should be true as they use the same method internally.
That sounds good to me! If we see that implementation changes too often for this to become a maintenance burden, we can reduce the testing.
And don't over-do it, having even some checks is already better than none at all 🙂 I would maybe also focus on a few checks that cover a lot of the behavior, rather than 5-10 very similar test cases. Furthermore, it's enough if the "base" versions (i.e. keys_shared()
and iter_shared()
) appear in these tests; all the QoL APIs on top use the same principles, and they already have their own tests.
Regarding just this point, i decided against adding that because there isn't (to my knowledge) a better way of iterating over only values other than iterating over |
godot-core/src/builtin/dictionary.rs
Outdated
} | ||
|
||
fn next_key(&mut self) -> Option<Variant> { | ||
let Some(next_var) = self.next_var.take() else { return None }; |
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.
Wait what is this magic syntax 🤯
Yeah ok googled it. Why did nobody tell me this exists now!
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 love let else, but it is actually pointless in this case.
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.
let else
is really awesome. Can't wait until clippy forces me to use it 😁
The non- Or go the other route: only have
Which lets you do:
It's more ceremony for tuples though, so a reusable iterator adaptor that just calls |
dict.keys_shared()
.map(GodotString::expect_from_variant)
.for_each(|s| println!("{s}")); sounds good, my suggestion here was to use something like the dict.keys_shared()
.typed::<GodotString>()
.for_each(|s| println!("{s}")); or, depending on type inference: dict.keys_shared()
.typed()
.for_each(|s: GodotString| println!("{s}")); Naming to be defined. Can also be something along "from_variant" (although not exactly that, it would lead to naming ambiguities). |
8b47fec
to
6025605
Compare
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.
Very nice, this PR starts to look really good! 😃
About TryFrom
implementations between Dictionary
and HashMap
: there was some discussion last week, generally going in the direction that once iterators and collect()
are available, TryFrom
is no longer needed.
What's the general opinion on this? I could imagine that Dictionary <-> HashMap
as well as Array <-> Vec
conversions happen relatively often. Do they deserve direct methods, or is iter_shared().typed::<..>().collect()
good enough?
bors try
godot-core/src/builtin/dictionary.rs
Outdated
} | ||
|
||
fn next_key(&mut self) -> Option<Variant> { | ||
let Some(next_var) = self.next_var.take() else { return None }; |
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.
let else
is really awesome. Can't wait until clippy forces me to use it 😁
tryBuild succeeded: |
3e01478
to
3ede7dc
Compare
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.
Mostly good, a few small comments left.
You can gladly squash the commits in the next change 🙂
bors try
godot-core/src/builtin/dictionary.rs
Outdated
GDExtensionConstVariantPtr, | ||
GDExtensionVariantPtr, | ||
*mut GDExtensionBool, | ||
) -> GDExtensionBool, |
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.
Please don't import sys::*
, but use explicit sys
prefixes.
I wrote down the code ready to copy-paste in my last comment 😉
godot-core/src/builtin/dictionary.rs
Outdated
/// SAFETY: | ||
/// `iter_fn` must point to a valid function that interprets the parameters according to their type specification. |
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.
For API documentation (///
), the convention is to add a new section (h1 as # Safety
).
The // SAFETY:
syntax is for code comments (//
).
Clippy should check the first.
/// SAFETY: | |
/// `iter_fn` must point to a valid function that interprets the parameters according to their type specification. | |
/// # Safety | |
/// `iter_fn` must point to a valid function that interprets the parameters according to their type specification. |
tryBuild succeeded: |
godot-core/src/builtin/dictionary.rs
Outdated
ffi_methods, interface_fn, GDExtensionBool, GDExtensionConstVariantPtr, GDExtensionVariantPtr, | ||
GodotFfi, | ||
}; | ||
use sys::{types::*, TagVariant}; |
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 import is outdated, instead write:
use sys::types::*;
Checking again with newly merged #109. This CI run should fail due to the import. bors try |
tryBuild failed: |
3ede7dc
to
f15d606
Compare
bors try |
tryBuild succeeded: |
# Conflicts: # itest/rust/src/dictionary_test.rs
Merged bors r+ |
Build succeeded: |
add 2 methods to dictionary:
iter_shared
for iterating over(Variant,Variant)
key-valueskeys_shared
for iterating overVariant
keysin addition iterators have:
.typed()
to convert an untyped iterator into a typed iterator