-
Notifications
You must be signed in to change notification settings - Fork 212
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
Fix Dictionary::get
unsoundness and add a few convenience functions
#748
Conversation
There are three methods which are very similar: fn get_or<K, D>(&self, default: D, key: K) -> Variant;
fn try_get<K>(&self, key: K) -> Option<Variant>;
fn get_non_nil<K>(&self, key: K) -> Option<Variant>; The difference between What I'm not exactly sure about, is whether Godot dictionaries allow storing nil values, or they treat it like absent values. In the former case, we would need to decide if we support both. I would argue that it is worthwhile to make the API closer to Rust's What about this? // same signature and semantics as HashMap::get(), except for returning a copy
fn get<K>(&self, key: K) -> Option<Variant>;
// equivalent to get(key).unwrap_or(Variant::new()), if someone actively uses the nil type
fn get_or_nil<K>(&self, key: K) -> Variant; |
Regarding this particular point:
I think there is a way to disambiguate between the two with what we have: by abusing referential equality with a sentinel object. We'll pass a new object as the default value to |
@Bromeon Yeah, Dictionaries do allow storing And another thing to consider is in GDScript Dictionaries behave differently. Trying to call |
@toasteater So you're saying pass in |
That's annoying indeed. To be honest, I would make it as user-friendly as possible. How many times do people use nil keys and values, especially if they're even hard to detect in GDScript? I would personally be OK with not supporting them (and adding such a feature if demand arises), or provide an "advanced" API which is a bit harder to use, but leaves the most common case simple to use.
Yeah, I think like in the other issue, we need to make a call if it's worth more to stay true to the Godot way of things, even if things are counter-intuitive, or to idiomatic Rust, even if it differs slightly from Godot. It's hard to meet a general decision, in this case I could imagine the advantages of an API closer to
This seems to be a bit far-fetched though, not sure if a user which is not familiar with the internals would come across this. But if some arcane way of achieving nil key/value helps to keep the more common use case clean, I'm not against it. |
@Bromeon I was just considering the feasibility, not the desirability, but I don't think null values are very useful in GDScript because, as Waridley said, the GDScript API provides no way to programmatically distinguish a |
I think my personal preference would be to keep |
The problem I see with pub fn get<K>(&self, key: &K) -> Option<Variant> {
self.contains(key).then(|| self.get_or_nil(key))
}
pub fn get_or<K, D>(&self, key: &K, default: D) -> Variant {
/* FFI stuff */
}
pub fn get_or_nil<K>(&self, key: &K) -> Variant {
self.get_or(key, Variant::new())
} ...so that |
rebased on godot-rust#746 & godot-rust#748 and squashed. Still writing more tests & awaiting final status on those PR's
Was too eager to push and go to bed, need to fix other uses of |
99f90bd
to
e152522
Compare
rebased on godot-rust#746 & godot-rust#748 and squashed. Still writing more tests & awaiting final status on those PR's
Should be good 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.
Thanks for the changes! 🙂
Still not 100% sure if the proposed API is actually the one that satisfies common use cases. It mostly mirrors the Godot API.
For example, when do we really want get_or_insert_nil()
? Above, we concluded that having null keys/values is not that useful, as the differentiation from absent values is hard -- and now we provide a method that inserts exactly that?
Typically, a user would insert his own custom value if there is no value yet, and for this, separate get()
+ update()
calls are good enough. I think that a frequent usage pattern might be:
let value = dict.get_or(key, default);
let updated_value = do_sth(value);
dict.update(key, updated_value);
|
||
/// Returns a copy of the value corresponding to the key, or `default` if it doesn't exist | ||
#[inline] | ||
pub fn get_or<K, D>(&self, default: D, key: K) -> Variant |
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.
Why is default
the first parameter? Having key
as first parameter for all 3 get*
methods is more intuitive, and consistent with GDScript.
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 was thinking of it in terms of Option::map_or
/Option::map_or_else
... Which admittedly aren't necessarily intuitive; I still have to think about them a bit when I use them. And I suppose this is a case where staying closer to GDScript is probably a good thing.
While However, I realize, 1) There is an alternative way to achieve this, and 2) As you mentioned in Discord, the main reason C++'s
This example would actually panic on We could also probably add |
You can't do that since the reference is built into the It's one of my pet peeves too but we'll just have to forget about |
@toasteater Ah, of course, duh. So the final verdict is remove |
I see. I would say for such cases it's better if we guide the users towards "idiomatic" usage patterns. There will be a few users that might need to adapt their code, but a lot of new users who don't wonder about the strange combination of API functions. If people really need something like this, we can always add it in the future.
That's also another thing I wanted to look at, but let's not broaden the scope of this PR too much. The naming and semantics of
For me yes, anything else @toasteater? |
It's a yes for me as well. Please go ahead! |
I thought of one more thing that could be added: |
pub fn get_or<K, D>(&self, key: K, default: D) -> Variant | ||
where | ||
K: ToVariant + ToVariantEq, | ||
D: ToVariant + ToVariantEq, |
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.
The default value does not need to be ToVariantEq
. The ToVariantEq
trait indicates that a type's Eq
implementation has the same behavior as its Godot counterpart. It's only useful for dictionary keys.
It might also be useful to relax the bound to OwnedToVariant
, as is done for insert
: https://github.com/godot-rust/godot-rust/blob/c02bd27ebee3df3035f7ae3294d205e00141b8e5/gdnative-core/src/core_types/dictionary.rs#L268
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.
Oh, I somehow didn't even see the line about relaxing the bound to OwnedToVariant
. However, it's probably best to track it as a separate issue as you have done anyway, as I'm not sure I fully understand the safety implications and when ToVariant
will still be required.
The |
Nevermind, it was just that both of the types I tried to make it an option of are impossible to distinguish from But on that note, why is |
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. Thanks!
Conversation about Option<()>
is continued in the serde PR.
I've created issue #752 to track trait bound inconsistency for Dictionary
methods.
bors r+
748: Fix `Dictionary::get` unsoundness and add a few convenience functions r=toasteater a=Waridley For some strange reason `godot_dictionary_get` calls `Dictionary::operator[]` instead of `Dictionary::get`, which ends up inserting `Nil` if the key doesn't already exist in the dictionary. https://github.com/godotengine/godot/blame/3.3/modules/gdnative/gdnative/dictionary.cpp#L126 This means it is unsound to call `godot_dictionary_get` on a `Dictionary<Shared>`. This PR re-implements godot-rust's `Dictionary::get` in terms of `godot_dictionary_get_with_default` (the behavior originally intended and likely expected by most Rust users), and adds `Dictionary::get_or_insert_nil` to provide the original behavior if it is indeed desired, but only on `Dictionary<T: LocalThreadAccess>` and an `unsafe` counterpart for `Dictionary<Shared>`, like the other mutating functions on `Dictionary`. I also updated the documentation of `get_ref` and `get_mut_ref` since they call `operator[]`. Perhaps they should also be moved to non-shared access, but I figured that's less important since they're unsafe anyway? I also added `try_get` and `get_non_nil` while I was at it, to simplify checking if a key exists and checking that its corresponding value is not `Nil` respectively. Co-authored-by: Waridley <[email protected]>
Build failed: |
Oh, wow, didn't realize |
Also adds `get_or` and `get_or_nil` convenience functions.
bors r+ |
Build succeeded: |
For some strange reason
godot_dictionary_get
callsDictionary::operator[]
instead ofDictionary::get
, which ends up insertingNil
if the key doesn't already exist in the dictionary.https://github.com/godotengine/godot/blame/3.3/modules/gdnative/gdnative/dictionary.cpp#L126
This means it is unsound to call
godot_dictionary_get
on aDictionary<Shared>
. This PR re-implements godot-rust'sDictionary::get
in terms ofgodot_dictionary_get_with_default
(the behavior originally intended and likely expected by most Rust users), and addsDictionary::get_or_insert_nil
to provide the original behavior if it is indeed desired, but only onDictionary<T: LocalThreadAccess>
and anunsafe
counterpart forDictionary<Shared>
, like the other mutating functions onDictionary
.I also updated the documentation of
get_ref
andget_mut_ref
since they calloperator[]
. Perhaps they should also be moved to non-shared access, but I figured that's less important since they're unsafe anyway?I also added
try_get
andget_non_nil
while I was at it, to simplify checking if a key exists and checking that its corresponding value is notNil
respectively.