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 Deque #807

Merged
merged 17 commits into from Sep 27, 2022
Merged

Add Deque #807

merged 17 commits into from Sep 27, 2022

Conversation

chipshort
Copy link
Contributor

@chipshort chipshort commented Sep 26, 2022

closes #776

Implements a deque, similar to the std lib VecDeque.
This allows having both queue and stack semantics with a single data structure.
I used wrapping math, such that, in case the indexes ever reach u32::MAX or you always push to the front, it will still work (as long as it's not filled completely).
No random access at the moment, but can add that, if desired.

The implementation is inspired by #786

Copy link
Contributor

@uint uint left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general, I really appreciate this PR. It's thorough, some important edge cases are covered, convention is followed, there's plenty of documentation, tests, and there's some clever stuff in there. Nice.

packages/storage-plus/README.md Outdated Show resolved Hide resolved
packages/storage-plus/README.md Outdated Show resolved Hide resolved
age: 123,
};

// use it like a queue by pushing and popping at opposite ends
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice examples!

packages/storage-plus/src/deque.rs Show resolved Hide resolved

/// Helper method for `tail` and `head` methods to handle reading the value from storage
fn read_meta_key(&self, storage: &dyn Storage, key: &[u8]) -> StdResult<u32> {
let full_key = namespaces_with_key(&[self.namespace], key);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using an existing helper that handles length encoding 👍

packages/storage-plus/src/deque.rs Show resolved Hide resolved

/// Tries to get the value at the given position
/// Used internally
fn get_at_unchecked(&self, storage: &dyn Storage, pos: u32) -> StdResult<Option<T>> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Should we just name it get_unchecked? It feels like array accessors in std are usually named get, not get_at.
  2. Since it costs us nothing, would you want to provide a public and checked get variant of this, and then call the type VecDeque, like you suggested before? I think it makes a lot of sense.

/// Adds the given value to the front of the deque
pub fn push_front(&self, storage: &mut dyn Storage, value: &T) -> StdResult<()> {
// need to subtract first, because head potentially points to existing element
let pos = self.head(storage)?.wrapping_sub(1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clever. But I guess this effectively means there's a length limit to these deques, and once someone goes beyond that limit, there's undefined behavior. I feel this should be documented and/or we should guard the user from that. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the limit is u32::MAX. We could guard against this when pushing by checking whether head and tail are equal and throwing an error. We will have to do an additional read from storage (of head or tail) for that though.
Which one do you prefer? Documenting or checking?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you store the head and tail indexes under one storage key, this would become just one read.

Either way works for me. I guess if we document it and someone requests guards or bigger arrays, we can always add that later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like std::collections::VecDeque will probably panic when grown beyond usize. I think we're fine just documenting.

packages/storage-plus/src/deque.rs Outdated Show resolved Hide resolved
@uint uint mentioned this pull request Sep 26, 2022
@uint uint self-requested a review September 27, 2022 10:50
Copy link
Contributor

@uint uint left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Feel free to merge once it's up to date with main and tests pass.

@webmaster128
Copy link
Member

Very nice. Looks like a JavaScript Array now.

Can you explain the reasoning for renaming Deque to VecDeque? It looks like a deque but it is not based in a vector (in contrast to std::collections::VecDeque).

@uint
Copy link
Contributor

uint commented Oct 3, 2022

Can you explain the reasoning for renaming Deque to VecDeque? It looks like a deque but it is not based in a vector (in contrast to std::collections::VecDeque).

I suggested it. Fair point. I guess I got swayed by the fact it implements indices and random access. It isn't resizable though. Would you prefer something like ArrDeque, or just Deque?

@webmaster128
Copy link
Member

Would you prefer something like ArrDeque, or just Deque?

I vote for just Deque – nice and simple.

I think here is no need to add implementation details in the name itself. Rust tends to do that because it matters at the system language level and it allows them to communicate their version is not a linked list, which has certain overhead.

Comment on lines +222 to +223
#[cfg(feature = "iterator")]
impl<'a, T> Iterator for VecDequeIter<'a, T>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The range/iterator implementation is genius.

Do I understand correctly that this even works without the native iterator implementation? I think you can remove #[cfg(feature = "iterator")] since you don't rely on storage ranges.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it only needs storage.get. Actually, I was wondering about that iterator feature. Are there CosmWasm chains that don't support storage.range?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC yes, there are chains that don't support it due to how their storage works. I'm sure Simon or Ethan could elaborate.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are underlying storage engines that don't have iterators. The default storage has it but e.g. Secret Network has an encrypted KV storage which you cannot iterate. Also Patricia Merkle Tries (Ethereum) do not support iterating.

@chipshort
Copy link
Contributor Author

So, should I make a follow-up PR renaming to Deque and with the iterator feature checks removed?

@uint
Copy link
Contributor

uint commented Oct 4, 2022

So, should I make a follow-up PR renaming to Deque and with the iterator feature checks removed?

That would be great!

@chipshort chipshort mentioned this pull request Oct 4, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add stack and queue implementations to storage-plus
3 participants