Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign upRFC: Linked list cursors #2570
Conversation
This comment has been minimized.
This comment has been minimized.
|
This RFC seems quite vague on the specifics. For example:
This seems like the kind of thing that might be better implemented as a crate first - given the amount of new API surface being added, I don't see how it could ever be accepted without at least a sample implementation. |
Centril
added
the
T-libs
label
Oct 21, 2018
This comment has been minimized.
This comment has been minimized.
4e554c4c
commented
Oct 21, 2018
|
@Diggsey good points I'll work on a reference implementation so this can be accepted |
This comment has been minimized.
This comment has been minimized.
4e554c4c
commented
Oct 22, 2018
|
Currently working on the implementation. All is done except for splitting lists. See here: https://github.com/4e554c4c/list_cursors |
This comment has been minimized.
This comment has been minimized.
|
Perhaps this could be an eRFC to allow greater experimentation? I would really like to play around with your implementation on nightly before deciding if we want to keep it as is, modify it, or go a different route. |
This comment has been minimized.
This comment has been minimized.
|
@mark-i-m the API would land unstable regardless of the e-ness of the RFC. |
This comment has been minimized.
This comment has been minimized.
|
@sfackler Yes, but the e-ness allows us to proceed without as clear of a vision on what API we will eventually adopt. In contrast, IIUC, if we accept a normal RFC for this, we are saying that we are reasonably confident in this approach. I think the approach has merit and is worth trying out, but I can't personally support strongly accepting the RFC because I simply don't have any experience with such an API. |
This comment has been minimized.
This comment has been minimized.
4e554c4c
commented
Oct 22, 2018
|
I'm still learning the Rust RFC process. How would I submit an eRFC?
|
This comment has been minimized.
This comment has been minimized.
|
@4e554c4c put an "e" before "RFC" ^.^ There's not much difference other than a change in intent. |
4e554c4c
changed the title
RFC: Linked list cursors
eRFC: Linked list cursors
Oct 22, 2018
This comment has been minimized.
This comment has been minimized.
4e554c4c
commented
Oct 22, 2018
|
I have edited the title to show that this is an eRFC. The API is not finalized and possibly incomplete and I am taking suggestions. |
This comment has been minimized.
This comment has been minimized.
|
The cursor API that you propose seems to be almost identical to the one that I implemented a while ago for One thing to note in particular as to how cursors work in my crate:
Another difference is that I have Regarding |
This comment has been minimized.
This comment has been minimized.
4e554c4c
commented
Oct 25, 2018
|
@Amanieu thank you for commenting! |
This comment has been minimized.
This comment has been minimized.
xaberus
commented
Oct 25, 2018
|
If I am not mistaken, the proposed API is not safe, i.e. let mut c = list.cursor_mut();
c.move_next();
// here we create a mutable reference to an element
let u = c.current();
c.move_prev();
// drop the element
drop(c.pop());
// here we drop this element
let mut dangle = u.unwrap();
// dereferencing a dangling reference to the now no longer existing element
**dangle = &777;
// use after free confirmed by valgrind...(I am still new to rust, so take the following with a grain of salt...) The reason is that in the code above Now we have basically created two mutable references to the same data: I think one way to prevent this is to separate the operation of editing the list and editing its elements, i.e. another If I remember correctly this is the reason, we have no similar interface in the std... N.B. This is a nice reference highlighting some interesting problems. |
This comment has been minimized.
This comment has been minimized.
4e554c4c
commented
Oct 25, 2018
|
Interesting! I thought that the reference would borrow the cursor (like how IndexMut borrows slices). |
This comment has been minimized.
This comment has been minimized.
xaberus
commented
Oct 25, 2018
|
@4e554c4c having given it a little more thought I think the prototype for pub fn current(&mut self) -> Option<&mut T> {...}
// that is elided to
pub fn current<'b>(&'b mut self) -> Option<&'b mut T> {...}not pub fn current(&mut self) -> Option<&'a mut T> {...}The first gives the reference a lifetime that is smaller than |
This comment has been minimized.
This comment has been minimized.
4e554c4c
commented
Oct 25, 2018
•
|
yep! That seems to fix the problem. The lifetime annotation I added seems to have made this worse. I'm going to look at the lifetimes more and update the RFC fn main() {
let mut list = LinkedList::new();
let mut c = list.cursor_mut();
c.insert(3);
c.insert(2);
c.insert(1);
let u = {
let mut c = c.as_cursor();
c.move_next();
c.current();
};
drop(c.pop());
// use after free!
println!("element: {:?}", u);
} |
This comment has been minimized.
This comment has been minimized.
4e554c4c
commented
Oct 25, 2018
|
Ok, lifetimes should be added to the RFC and fixed in the reference implementation. Tell me what y'all think. |
This comment has been minimized.
This comment has been minimized.
xaberus
commented
Oct 25, 2018
•
|
@4e554c4c: I think the example is actually OK. (I think you meant with the modified signatures impl<'a, T> Cursor<'a, T> {
...
pub fn current(&self) -> Option<&T> {...}
...
}
...
impl<'a, T> CursorMut<'a, T> {
...
pub fn current(&mut self) -> Option<&mut T> {...}
...
}the borrow checker (on nightly) complains about |
This comment has been minimized.
This comment has been minimized.
xaberus
commented
Oct 25, 2018
If my reading of the nomicon is correct ( pub fn as_cursor<'cm>(&'cm self) -> Cursor<'cm, T>;By elision rules this should be the same as pub fn as_cursor(&self) -> Cursor<T>;(clippy nags about it...) |
This comment has been minimized.
This comment has been minimized.
|
To be precise, the methods on |
This comment has been minimized.
This comment has been minimized.
|
Actually, this would probably work: impl<'a, T> Cursor<'a, T> {
...
pub fn current(&self) -> Option<&'a T> {...}
...
}
...
impl<'a, T> CursorMut<'a, T> {
...
pub fn current_mut(&mut self) -> Option<&mut T> {...}
pub fn current(&self) -> Option<&T> {...}
// Same with peek/peek_mut, etc
...
}The advantage of having |
This comment has been minimized.
This comment has been minimized.
4e554c4c
commented
Oct 25, 2018
•
|
Good point, I'll leave that one how it was. |
4e554c4c
force-pushed the
4e554c4c:cursors
branch
from
524f9f8
to
67d797b
Oct 25, 2018
This comment has been minimized.
This comment has been minimized.
4e554c4c
commented
Oct 28, 2018
|
The reference implementation should be pretty much complete thanks to @xaberus |
This comment has been minimized.
This comment has been minimized.
xaberus
commented
Oct 30, 2018
•
|
Having tinkered with the reference implementation for a while, here are my impressions and comments so far.
I hope to continue this list when I have time to work on it. What are your comments so far? |
This comment has been minimized.
This comment has been minimized.
4e554c4c
commented
Oct 30, 2018
|
I think this can work. The main reason that If the cursor doesn't start at the empty element, could we just do away with the empty element altogether? We could have the invariant that the The only problem I see with this is that we would need to add some extra method to determine whether the cursor is at the beginning/end of a list, e.g. |
This comment has been minimized.
This comment has been minimized.
blankname
commented
Oct 30, 2018
This comment has been minimized.
This comment has been minimized.
|
The libs team discussed this RFC and decided to FCP merge as a normal RFC. The API surface area proposed here does not need the procedural overhead of an eRFC. There are some open design questions around the specific semantics (e.g. wrapping vs non-wrapping) but those can be worked out in the tracking issue for the feature. @rfcbot fcp merge |
This comment has been minimized.
This comment has been minimized.
rfcbot
commented
Nov 2, 2018
•
|
Team member @sfackler has proposed to merge this. The next step is review by the rest of the tagged teams: No concerns currently listed. Once a majority of reviewers approve (and none object), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! See this document for info about what commands tagged team members can give me. |
rfcbot
added
proposed-final-comment-period
disposition-merge
labels
Nov 2, 2018
Centril
changed the title
eRFC: Linked list cursors
RFC: Linked list cursors
Nov 2, 2018
This comment has been minimized.
This comment has been minimized.
xaberus
commented
Nov 3, 2018
|
As this is my first collaboration on an RFC, what is the proper way to propose changes to the RFC? Right now a RFC is a pull-request so I cannot have a pull-request to a pull request, can I? In my non-wrapping branch/playground I ended up with a slightly modified API, that is more symmetric and explicit as to what the individual methods to. In particular:
with the semantics as written in the referenced gist and invariants as listed in my post above. |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
4e554c4c
commented
Nov 4, 2018
|
Thanks for the comments and sorry for not being able to respond earlier. I'm a big fan of the convenience methods to get a cursor at the beginning or end of a list, but I think the Also this may not be a problem, but to me |
This comment has been minimized.
This comment has been minimized.
xaberus
commented
Nov 4, 2018
•
|
@4e554c4c: Regarding list.cursor().before_head();
list.cursor().head();
list.cursor().tail();
list.cursor().after_tail();
list.cursor_mut().before_head();
list.cursor_mut().head();
list.cursor_mut().tail();
list.cursor_mut().after_tail();(I.e. all the cases the API must cover...) To me this feels a lot more verbose than just using an enum. Any other ideas? |
This comment has been minimized.
This comment has been minimized.
|
A builder pattern won't work for mutable vs immutable cursors since they require An enum was suggested: enum Pos {
BeforeFirst,
First, // Same as AfterLast if list is empty
Last, // Same as BeforeFirst if list is empty
AfterLast,
}
list.cursor(Pos::First);
list.cursor_mut(Pos::AfterLast);Or a builder: list.cursor().first()
list.cursor_mut().after_last(); |
This comment has been minimized.
This comment has been minimized.
xaberus
commented
Nov 4, 2018
•
This is what I wrote in one of my first attempts, but it is kind of surprising in the case of an empty list:
v.s.
If I request a cursor at the |
This comment has been minimized.
This comment has been minimized.
|
@xaberus Note that a wrapping cursor (where BeforeFirst is the same as AfterLast) avoids this problem. This is why I went with that approach in |
This comment has been minimized.
This comment has been minimized.
xaberus
commented
Nov 4, 2018
|
@Amanieu I see. In way, this not a real problem: The invariants (sans the last one) enforce a wrapping behavior for inserts which effectively collapses the cursor states for the empty list to // non-wrapping
let c = list.cursor(BeforeFirst);
for _ in 0..n {
c.move_next();
}
c.current()v.s. // wrapping
let c = list.cursor(BeforeFirst);
c.move_next();
for _ in 0..n {
if c.current().is_none() {
return None;
} else {
c.move_next();
}
}
c.current() |
rfcbot
added
final-comment-period
and removed
proposed-final-comment-period
labels
Nov 6, 2018
This comment has been minimized.
This comment has been minimized.
rfcbot
commented
Nov 6, 2018
|
|
This comment has been minimized.
This comment has been minimized.
|
The |
This comment has been minimized.
This comment has been minimized.
|
I would prefer if some changes were made to the method names. In particular, I think that every method name should explicitly specify the direction in which the operation is performed (forwards or backwards).
|
This comment has been minimized.
This comment has been minimized.
nugend
commented
Nov 7, 2018
|
I'm just driving by, but @Amanieu has got it right. This is not a clunky interface at all as long as the meaning of those API methods are really clear and unambiguous about which elements are being affected. This RFC actually strongly reminds me of the more general Zipper concept that Haskell has explored at length (and linked lists are just degenerate trees). I suspect that's not really pertinent to the matter at hand, but I think it's interesting. |
This comment has been minimized.
This comment has been minimized.
|
@Amanieu Aiming for symmetry with explicitly specified directions sounds good; however, doesn't this bit present a smidgen of discord in the plan?
Why to the next element rather than the previous? Why aren't there then two versions of it, with explicitly specified directions, like the others? Of course as a matter of pragmatics, you more often want to move forwards rather than backwards, but if we're choosing not to privilege the forwards direction in the rest of the API... |
This comment has been minimized.
This comment has been minimized.
xaberus
commented
Nov 7, 2018
|
@glaebhoerl: I second that. Removing an item usually can be decided by a predicate let mut c = list.cursor_mut(BeforeHead);
while c.peek_next().is_some() {
if let Some(true) = c.peek_next().map(f) {
c.pop_next();
} else {
c.move_next();
}
}sounds like a reasonable interface that is symmetric by replacing |
This comment has been minimized.
This comment has been minimized.
eaglgenes101
commented
Nov 9, 2018
•
|
I think that it might be possible to allow for multiple mutable cursors, safely, with a method that splits out two bounded cursors. These bounded cursors will then recognize the position at which the cursor was split, and if the linked list element pointed to is that node, following that pointer through the bounded cursor API instead goes to that bounded cursor's sentinel, and looping back around from the far end will go to the node adjacent to the split-at node on the bounded cursor's side instead of to the other end. Splitting at the sentinel doesn't make sense, so if a split is attempted at the sentinel, then pub fn split_at_mut<'cm>(&'cm mut self) -> Option<(BoundedCursorMut<'cm, T>, &'cm mut T, BoundedCursorMut<'cm, T>)>;(The node in the middle where the list is split is a no man's land for both bounded cursors, so that if the resulting bounded cursors are used to manipulate the nodes bordering the split, they don't have to change the pointers on the other side's nodes to retain linked list consistency, instead only having to change the middle node's pointer pointing towards their side.) |
This comment has been minimized.
This comment has been minimized.
rfcbot
commented
Nov 16, 2018
|
The final comment period, with a disposition to merge, as per the review above, is now complete. |
rfcbot
added
finished-final-comment-period
and removed
final-comment-period
labels
Nov 16, 2018
Centril
added
A-types-libstd
A-collections
labels
Nov 22, 2018
This comment has been minimized.
This comment has been minimized.
|
Anyone going to make a tracking issue? |
This comment has been minimized.
This comment has been minimized.
|
@clarcharr I usually made them but I missed this one for some reason... @sfackler given the recent discussion, can someone from T-libs create the tracking issue and merge the RFC to make sure that y'all have taken in and are OK with the recent discussion? |
This comment has been minimized.
This comment has been minimized.
nitnelave
commented
Dec 9, 2018
•
|
Just my grain of salt, but I have used a similar API (https://contain-rs.github.io/linked-list/linked_list/struct.Cursor.html) for a circular linked_list, and the ghost element made it hard to move around since I wasn't interested in where was the beginning/end of the list. The solution I had was to implement seek_forward/seek_backward methods that ensured that the next element was never the ghost, but it was clunky. Would it be somehow possible to have a way to say "I don't want a ghost"? i.e. the ghost would only be here for the empty list, but as soon as you insert an element, it becomes a loop to itself. We could potentially have it as a generics parameter? (not sure about it) |
4e554c4c commentedOct 21, 2018
•
edited
This is my first RFC, so feel free to critique :)
Many of the benefits of linked lists rely on the fact that most operations (insert, remove, split, splice etc.) can be performed in constant time once one reaches the desired element. To take advantage of this, a
Cursorinterface can be created to efficiently edit linked lists. Furthermore, unstable extensions like theIterMutchanges will be removed.The reference implementation is here, feel free to make implementation suggestions. :)
split,split_beforesplit lists (this seems to be ambiguous in the specification)