Skip to content
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

Merged
merged 2 commits into from
Feb 11, 2023

Conversation

lilizoey
Copy link
Member

@lilizoey lilizoey commented Jan 31, 2023

add 2 methods to dictionary:

  • iter_shared for iterating over (Variant,Variant) key-values
  • keys_shared for iterating over Variant keys

in addition iterators have:

  • .typed() to convert an untyped iterator into a typed iterator

Copy link
Member

@Bromeon Bromeon left a 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 Show resolved Hide resolved
Comment on lines 210 to 212
/// 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.
Copy link
Member

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 Show resolved Hide resolved
godot-core/src/builtin/dictionary.rs Outdated Show resolved Hide resolved
Comment on lines 356 to 365
// The function returns a bool
assert!(success == 0 || success == 1);
let success = success != 0;

if success {
assert!(valid);
Some(variant)
} else {
None
}
Copy link
Member

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}"),
}

Copy link
Member Author

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

Copy link
Member

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 😉

godot-core/src/builtin/dictionary.rs Outdated Show resolved Hide resolved
godot-core/src/builtin/dictionary.rs Outdated Show resolved Hide resolved
itest/rust/src/dictionary_test.rs Outdated Show resolved Hide resolved
Comment on lines 494 to 496
// The below tests, test to ensure no panicking happens when mutating
// a dictionary while iterating. Though the result of doing is not
// guaranteed.
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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 🙂

Copy link
Member Author

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.

Copy link
Member

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.

itest/rust/src/dictionary_test.rs Outdated Show resolved Hide resolved
@lilizoey
Copy link
Member Author

lilizoey commented Feb 2, 2023

[...]
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 grimacing
[...]

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 values_array() or some equivalent method.

@Bromeon
Copy link
Member

Bromeon commented Feb 2, 2023

Any feedback on the iterator adaptors I mentioned here?
Maybe also from @ttencate, as you worked a lot on arrays?

}

fn next_key(&mut self) -> Option<Variant> {
let Some(next_var) = self.next_var.take() else { return None };
Copy link
Contributor

@ttencate ttencate Feb 2, 2023

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!

Copy link
Member Author

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.

Copy link
Member

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 😁

@ttencate
Copy link
Contributor

ttencate commented Feb 2, 2023

Any feedback on the iterator adaptors I mentioned here? Maybe also from @ttencate, as you worked a lot on arrays?

The non-_as versions could maybe be omitted, since Variant also implements FromVariant? We just decided that the extra copy is not an issue for arrays, so it shouldn't be an issue here either. Unfortunately, generic arguments to functions can't have defaults, so <K: FromVariant = Variant> won't work. Fortunately, iterating over raw Variants is probably not very common so users will have to specify a type most of the time anyway.

Or go the other route: only have Variant iterators but add a helper to FromVariant:

trait FromVariant: Sized {
    ...
    fn expect_from_variant(v: Variant) -> Self {
        Self::try_from_variant(&v).unwrap()
    }
}

Which lets you do:

dict.keys_shared()
    .map(GodotString::expect_from_variant)
    .for_each(|s| println!("{s}"));

It's more ceremony for tuples though, so a reusable iterator adaptor that just calls FromVariant sounds nice. Whether it's useful for arrays depends on how typed arrays will turn out...

@Bromeon
Copy link
Member

Bromeon commented Feb 2, 2023

dict.keys_shared()
    .map(GodotString::expect_from_variant)
    .for_each(|s| println!("{s}"));

sounds good, my suggestion here was to use something like the cloned() standard adapter:

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).

@lilizoey lilizoey requested a review from Bromeon February 3, 2023 17:32
@Bromeon Bromeon added feature Adds functionality to the library c: core Core components labels Feb 3, 2023
Copy link
Member

@Bromeon Bromeon left a 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 Show resolved Hide resolved
godot-core/src/builtin/dictionary.rs Outdated Show resolved Hide resolved
godot-core/src/builtin/dictionary.rs Outdated Show resolved Hide resolved
godot-core/src/builtin/dictionary.rs Outdated Show resolved Hide resolved
godot-core/src/builtin/dictionary.rs Outdated Show resolved Hide resolved
itest/rust/src/dictionary_test.rs Outdated Show resolved Hide resolved
itest/rust/src/dictionary_test.rs Show resolved Hide resolved
itest/rust/src/dictionary_test.rs Outdated Show resolved Hide resolved
}

fn next_key(&mut self) -> Option<Variant> {
let Some(next_var) = self.next_var.take() else { return None };
Copy link
Member

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 😁

itest/rust/src/dictionary_test.rs Outdated Show resolved Hide resolved
bors bot added a commit that referenced this pull request Feb 3, 2023
@bors
Copy link
Contributor

bors bot commented Feb 3, 2023

try

Build succeeded:

@lilizoey lilizoey force-pushed the feature/dictionary-iter branch 2 times, most recently from 3e01478 to 3ede7dc Compare February 4, 2023 00:24
@lilizoey lilizoey marked this pull request as ready for review February 4, 2023 00:30
Copy link
Member

@Bromeon Bromeon left a 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 Show resolved Hide resolved
Comment on lines 350 to 353
GDExtensionConstVariantPtr,
GDExtensionVariantPtr,
*mut GDExtensionBool,
) -> GDExtensionBool,
Copy link
Member

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 😉

Comment on lines 346 to 344
/// SAFETY:
/// `iter_fn` must point to a valid function that interprets the parameters according to their type specification.
Copy link
Member

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.

Suggested change
/// 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.

bors bot added a commit that referenced this pull request Feb 4, 2023
@bors
Copy link
Contributor

bors bot commented Feb 4, 2023

try

Build succeeded:

ffi_methods, interface_fn, GDExtensionBool, GDExtensionConstVariantPtr, GDExtensionVariantPtr,
GodotFfi,
};
use sys::{types::*, TagVariant};
Copy link
Member

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::*;

@Bromeon
Copy link
Member

Bromeon commented Feb 4, 2023

Checking again with newly merged #109. This CI run should fail due to the import.

bors try

bors bot added a commit that referenced this pull request Feb 4, 2023
@bors
Copy link
Contributor

bors bot commented Feb 4, 2023

try

Build failed:

@Bromeon Bromeon mentioned this pull request Feb 5, 2023
@Bromeon
Copy link
Member

Bromeon commented Feb 5, 2023

bors try

bors bot added a commit that referenced this pull request Feb 5, 2023
@bors
Copy link
Contributor

bors bot commented Feb 5, 2023

try

Build succeeded:

# Conflicts:
#	itest/rust/src/dictionary_test.rs
@Bromeon
Copy link
Member

Bromeon commented Feb 11, 2023

Merged master into this branch. Thanks a lot!

bors r+

@bors
Copy link
Contributor

bors bot commented Feb 11, 2023

Build succeeded:

@bors bors bot merged commit 18f3a7b into godot-rust:master Feb 11, 2023
@lilizoey lilizoey deleted the feature/dictionary-iter branch April 16, 2023 16:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c: core Core components feature Adds functionality to the library
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants