-
Notifications
You must be signed in to change notification settings - Fork 265
feat: caching LookupMap implementation #487
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
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.
just some comments about naming, but overall looking good
near-sdk/src/utils/stable_map.rs
Outdated
@@ -40,4 +41,12 @@ impl<K, V> StableMap<K, V> { | |||
pub(crate) fn inner(&mut self) -> &mut BTreeMap<K, Box<V>> { | |||
self.map.get_mut() | |||
} | |||
pub(crate) fn with_value<Q: ?Sized, F, T>(&self, k: &Q, f: F) -> Option<T> |
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 don't think we should prefix this function with with_
since those have been more primarily associated with the creation of the struct itself. Maybe name it something like map_value
?
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.
My thinking was that this is not actually mapping the value since it's just calling a function on the reference to the value, not moving the owned value to be mapped. This might just be semantics, but map would be FnOnce(V) -> T
when this is actually FnOnce(&V) -> T
If you feel strongly about this I'm happy to change since I don't really have an opinion here and it's a private API
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.
ahh I see. I don't have too strong of an opinion what this should be either. And huh, VaListImpl::with_copy has a similar signature, so it's not too bad for it to have a prefix of with_
but certainly very uncommon
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.
renamed to map_value_ref
. I don't really think there is a great name, but it's internal and this might be the clearest. Sound good to you?
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.
Yup that sounds good to me. Thanks
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.
Did a very quick look, will continue tomorrow!
/// assert_eq!(map["poneyland"], "hoho".to_string()); | ||
/// ``` | ||
pub fn or_insert_with<F: FnOnce() -> V>(self, default: F) -> &'a mut V { | ||
match self { |
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 could delegate to or_insert_with_key
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.
Does this get compiled away? I just noticed that std::collections::HashMap
does not do this and decided to follow their pattern.
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.
Yes, according to my mental model, the compiler should trivially compile that away. This pattern is used by Option
and once cell: https://github.com/rust-lang/rust/blob/cdeba02ff71416e014f7130f75166890688be986/library/core/src/option.rs#L1228
It's interesting that maps don't do that. I suppose that's historical -- this is the form the API was added before 1.0, and the form it was copy-pasted into hash brown: rust-lang/rust#22930.
No strong opinion here though -- if you'll feel more confident with the current impl, that's totally fine!
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.
No, that's fine, I'll switch. I'm just noting the reason I had was that I assumed because it was the pattern in std::.::HashMap that there was a benefit
match self { | ||
Self::Occupied(entry) => entry.into_mut(), | ||
Self::Vacant(entry) => entry.insert(default), | ||
} |
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 can delegate to or_insert_with
match self { | |
Self::Occupied(entry) => entry.into_mut(), | |
Self::Vacant(entry) => entry.insert(default), | |
} | |
self.or_insert_with(|| default) |
That way, there's a single place which contains the match.
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.
same here as above, I'm following std::HashMap pattern. I'll try to dig into comparing what the two compile to exactly to make sure there is no difference
K: Borrow<Q>, | ||
Q: BorshSerialize + ToOwned<Owned = K> + Ord, |
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.
Huh, that's subtle!
I think here we have an extra requrenment that Q: BorshSerialize
and K: BorshSerialize
use the same serialization format. IE, this extends the usual Borrow
contract to cover serialization format as well.
I don't see any immediate problems here.
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, it's not ideal. I couldn't think of a cleaner way to handle this, though. Maybe we can document just to make sure this assumption is clear. Assuming K
and Borrow<K>
have the same serialization seems in line with how Borrow
is defined in some places, but others indicate that it only covers Hash
, Eq
, Ord
where | ||
K: Clone, |
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.
suggestion(blocking for stabilization): the K: Clone
bound doesn't make sense -- we already have an owned K
here. Can we write
impl StableMap {
fn get_key_value_mut(&mut self, k: K) -> (&mut K, &mut V) { ... }
}
sounds doable in theory?
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.
Intuitively, it makes sense, but I don't know how you're getting the &mut K
, let alone a &K
through the entry API like this.
I'll come back to this to see if there is anything that can be done, but at best with some code duplication all I can come up with is removing one clone, which I'm not certain is very helpful
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.
Didn't do a second pass through, but this certainly feel ready as an unstable feature.
* master: feat: caching LookupMap implementation (#487)
The interface closely matches a
std::collections::HashMap
without the functions that require being aware of all keys (as this does not store keys, another data structure will be built on this for that) and adding on functions that are useful in this context such as:set
: update value without loading the existing value from state and returning replacedflush
: To write cached modifications to storage manually (done automatically onDrop
, but this could be useful for if measuring storage usage before and after and not requiring drop)