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 key_mut method to BTreeMap entries #112896

Closed

Conversation

clarfonthey
Copy link
Contributor

@clarfonthey clarfonthey commented Jun 21, 2023

This implements part of the proposal in this comment to improve the BTreeMap cursor API.

I'm not a huge fan of including an entire Ord implementation in a doc example but this feels like the best way to show off how to use these methods. Definitely open to feedback on that.

For now, I've included the cursor API tracking issue as the tracking issue for these but a separate feature flag, since I feel these are technically disjoint from it. Would be open to separating it into a different tracking issue.

Also open to feedback on the safety guarantee for VacantEntry. Definitely open to changing that, but I figured being the most cautious upfront would be good, since we can always loosen the guarantees later.

ACP: rust-lang/libs-team#356

@rustbot
Copy link
Collaborator

rustbot commented Jun 21, 2023

r? @m-ou-se

(rustbot has picked a reviewer for you, use r? to override)

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Jun 21, 2023
@rust-log-analyzer

This comment has been minimized.

@clarfonthey
Copy link
Contributor Author

(side note, apparently ./x test library/std didn't include the doc tests for alloc 😞)

@rust-log-analyzer

This comment has been minimized.

@ripytide
Copy link
Contributor

Reposting my comment from #118208 (comment) here slightly adapted as I believe it is similarly relevant:

Would it not be a better idea to rather than creating this unsafe key_mut() method to have unlimited &mut key access, to instead expose some function fn try_replace_key(&mut self, new_key: K) -> Result<K, InvalidKeyError> on OccupiedEntry which would check if the new key held up to the Ord invariants with the surrounding keys when doing the replacement.

I suppose the disadvantage would be that performance wise you would always do up to two comparisons per key replacement. Perhaps then we could also add an unsafe fn replace_key_unchecked(&mut self, new_key: K) variant as well?

@clarfonthey
Copy link
Contributor Author

I could add that, but the main reason why I prefer a key_mut API regardless is that most of these cases involve modifying data inside the key that isn't related to the Ord implementation, and having to do some kind of replace operation might be too costly since it could require reallocating the contents of the key.

I also am not sure how to perform those checks in the code at the moment, but I could probably figure it out.

@ripytide
Copy link
Contributor

Hmm, that use-case sounds odd, why would you have data in the key not related to the Ord implementation that needs mutating, could you not move those things into the value type?

@clarfonthey
Copy link
Contributor Author

clarfonthey commented Nov 26, 2023

Basically the primary case that was brought up when making this API was range maps, where your keys are ranges of values instead of values themselves. A common way of implementing this is to make your Ord implementation only sorts by the start of the range, then later ensure that the ranges in the map don't overlap by removing some keys and modifying others to extend them to a larger area.

So, having to re-check that the start of the range here is valid when that's an invariant that must be upheld here anyway doesn't really help. And since you're probably going to make breaking this invariant unsafe, there's not really a point to having a "safe" version of the re-insert code since you're already in unsafe territory anyway.

But I also am making a lot of assumptions here, so, your argument is probably also valid. It's just not what was on my mind when writing it!

Also, it's worth adding that you're right that these bits could be stored in the value instead. It's just easier to make the map go from Range<uN> to V instead of uN to (uN, V) where the two halves of the range are split.

@jeffparsons
Copy link
Contributor

Also, it's worth adding that you're right that these bits could be stored in the value instead. It's just easier to make the map go from Range<uN> to V instead of uN to (uN, V) where the two halves of the range are split.

This is going back a bit, so my memory is hazy, but I think I tried both in the rangemap crate and settled on "store the range in the key" because it was much easier and enabled hacks like wrapper structs with different Ord implementations. So, yeah, basically what you said.

However, and I know this is a bit of a tangent to this work, I think all those concerns would be obviated by a "visitor" style search method that accepts a callback to decide where to go next ("found", "ascend", "descend right", etc.). Then the consumer of the API could use whatever information they want to decide where to go next. I don't know if that would be too low-level to expose in the public API but I've often found myself wishing I had something like that.

@clarfonthey
Copy link
Contributor Author

It's also worth mentioning that any "coalescing" behaviour for map keys could also benefit from key_mut since, for example, let's imagine that we use the schema mentioned where A: (B, C) is equivalent to indicating (A..B): C in our map.

If we have B: (C, X) inside our map and want to insert (A..B): X into it, then it makes sense to replace the existing key B with A, rather than to insert a new key A and delete B. In this case, we'd prefer key mutation over the APIs proposed.

Granted, there are multiple cases to be made for reducing unsafe code and not ever doing key mutation, instead preferring to replace keys, but at least for the sake of flexibility and replacing the existing proposed key mutation methods for cursors, this feels like a reasonable compromise. We can litigate whether key mutation APIs should really exist before this is stabilised, since at least for now, they do exist and people are using them.

@ripytide
Copy link
Contributor

ripytide commented Dec 9, 2023

However, and I know this is a bit of a tangent to this work, I think all those concerns would be obviated by a "visitor" style search method that accepts a callback to decide where to go next ("found", "ascend", "descend right", etc.). Then the consumer of the API could use whatever information they want to decide where to go next. I don't know if that would be too low-level to expose in the public API but I've often found myself wishing I had something like that.

I completely agree which is why I made https://github.com/ripytide/btree_monstrousity which essentially just removes the Ord bound from all methods on the BTreeMap and instead takes a closure that returns an Ordering to enable arbitrary search behavior just as you said.
It's what DiscreteRangeMap now uses instead of the Ord bodge which made the code much simpler and easier to reason about.

@Amanieu Amanieu assigned Amanieu and unassigned m-ou-se Feb 14, 2024
@Amanieu Amanieu added the I-libs-api-nominated Nominated for discussion during a libs-api team meeting. label Feb 21, 2024
/// # Safety
///
/// Mutating this key *does not* change the position of the entry, meaning that any mutation
/// should preserve the ordering of the key with respect to all the others in the map.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// should preserve the ordering of the key with respect to all the others in the map.
/// must preserve the ordering of the key with respect to all the others in the map.

@joshtriplett
Copy link
Member

@clarfonthey Could you please change the name to something like key_mut_unchecked?

That would leave more room for some potential future checked version, which couldn't return &mut but could for instance take a closure that modifies the key, and then after the modification could assert that the key still sorts correctly relative to the previous and next key. (We don't need to add that checked version in the same PR.)

@Amanieu Amanieu added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. I-libs-api-nominated Nominated for discussion during a libs-api team meeting. labels Feb 28, 2024
@clarfonthey
Copy link
Contributor Author

So, I am fine with that change, but after thinking about it a bit more, it feels probably better to do something like BinaryHeap::peek_mut instead, where you return a DerefMut wrapper whose Drop handler will do the verification.

I think we might be able to do something that is both sound and allows an unchecked version, which I think would presumably bypass Drop entirely and consume the wrapper instead. Will ponder that and try to draft up a version.

Since this feels a bit more complicated than the existing method, I'll also draft up an ACP for it.

@clarfonthey
Copy link
Contributor Author

@rustbot waiting-on-ACP

rust-lang/libs-team#356

@clarfonthey
Copy link
Contributor Author

@rustbot ACP

@Dylan-DPC Dylan-DPC added S-waiting-on-ACP Status: PR has an ACP and is waiting for the ACP to complete. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Jul 29, 2024
@clarfonthey
Copy link
Contributor Author

Gonna just close this since it doesn't match the current ACP. If the ACP gets accepted I'll open a new PR with the implementation.

@clarfonthey clarfonthey closed this Oct 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
S-waiting-on-ACP Status: PR has an ACP and is waiting for the ACP to complete. T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants