Skip to content

Commit 2735924

Browse files
authored
Merge pull request #2570 from 4e554c4c/cursors
RFC: Linked list cursors
2 parents 1ab8c3c + 71ec5ab commit 2735924

File tree

1 file changed

+317
-0
lines changed

1 file changed

+317
-0
lines changed

text/2570-linked-list-cursors.md

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
- Feature Name: `linked_list_cursors`
2+
- Start Date: 2018-10-14
3+
- RFC PR: [rust-lang/rfcs#2570](https://github.com/rust-lang/rfcs/pull/2570)
4+
- Rust Issue: [rust-lang/rust#58533](https://github.com/rust-lang/rust/issues/58533)
5+
6+
# Summary
7+
[summary]: #summary
8+
9+
Many of the benefits of linked lists rely on the fact that most operations
10+
(insert, remove, split, splice etc.) can be performed in constant time once one
11+
reaches the desired element. To take advantage of this, a `Cursor` interface
12+
can be created to efficiently edit linked lists. Furthermore, unstable
13+
extensions like the `IterMut` changes will be removed.
14+
15+
# Motivation
16+
[motivation]: #motivation
17+
18+
From Programming Rust:
19+
> As of Rust 1.12, Rust’s LinkedList type has no methods for removing a range of
20+
> elements from a list or inserting elements at specific locations in a list.
21+
> The API seems incomplete.
22+
23+
Both of these issues have been fixed, but in different and incompatible ways.
24+
Removing a range of elements is possible though the unstable `drain_filter` API,
25+
and inserting elements in at specific locations in a list is possible through
26+
the `linked_list_extras` extensions to `IterMut`.
27+
28+
This motivates the need for a standard interface for insertion and deletion of
29+
elements in a linked list. An efficient way to implement this is through the use
30+
of "cursors". A cursor represents a position in a collection that can be moved
31+
back and forth, somewhat like a `DoubleEndedIterator`. However, mutable cursors
32+
can also edit the collection at their position.
33+
34+
A mutable cursor would allow for constant time insertion and deletion of
35+
elements and insertion and splitting of lists at its position. This would allow
36+
for simplification of the `IterMut` API and a complete LinkedList
37+
implementation.
38+
39+
# Guide-level explanation
40+
[guide-level-explanation]: #guide-level-explanation
41+
42+
The cursor interface would provides two new types: `Cursor` and `CursorMut`.
43+
These are created in the same way as iterators.
44+
45+
With a `Cursor` one can seek back and forth through a list and get the current
46+
element. With a `CursorMut` One can seek back and forth and get mutable
47+
references to elements, and it can insert and delete elements before and behind
48+
the current element (along with performing several list operations such as
49+
splitting and splicing).
50+
51+
Lets look at where these might be useful.
52+
53+
## Examples
54+
55+
This interface is helpful most times insertion and deletion are used together.
56+
57+
For example, consider you had a linked list and wanted to remove all elements
58+
which satisfy a certain predicate, and replace them with another element. With
59+
the old interface, one would have to insert and delete separately, or split the
60+
list many times. With the cursor interface, one can do the following:
61+
62+
``` rust
63+
fn remove_replace<T, P, F>(list: &mut LinkedList<T>, p: P, f: F)
64+
where P: Fn(&T) -> bool, F: Fn(T) -> T
65+
{
66+
let mut cursor = list.cursor_mut();
67+
// move to the first element, if it exists
68+
loop {
69+
let should_replace = match cursor.peek() {
70+
Some(element) => p(element),
71+
None => break,
72+
};
73+
if should_replace {
74+
let old_element = cursor.pop().unwrap();
75+
cursor.insert(f(old_element));
76+
}
77+
cursor.move_next();
78+
}
79+
}
80+
```
81+
82+
This could also be done using iterators. One could transform the list into an
83+
iterator, perform operations on it and collect. This is easier, however it still
84+
requires much needless allocation.
85+
86+
For another example, consider code that was previously using `IterMut`
87+
extensions.
88+
``` rust
89+
fn main() {
90+
let mut list: LinkedList<_> = (0..10).collect();
91+
let mut iter = list.iter_mut();
92+
while let Some(x) = iter.next() {
93+
if x >= 5 {
94+
break;
95+
}
96+
}
97+
iter.insert_next(12);
98+
}
99+
```
100+
This can be changed almost verbatim to `CursorMut`:
101+
``` rust
102+
fn main() {
103+
let mut list: LinkedList<_> = (0..10).collect();
104+
let mut cursor = list.cursor_mut() {
105+
while let Some(x) = cursor.peek_next() {
106+
if x >= 5 {
107+
break;
108+
}
109+
cursor.move_next();
110+
}
111+
cursor.insert(12);
112+
}
113+
```
114+
In general, the cursor interface is not the easiest way to do something.
115+
However, it provides a basic API that can be built on to perform more
116+
complicated tasks.
117+
118+
# Reference-level explanation
119+
[reference-level-explanation]: #reference-level-explanation
120+
121+
One gets a cursor the exact same way as one would get an iterator. The
122+
returned cursor would point to the "empty" element, i.e. if you got an element
123+
and called `current` you would receive `None`.
124+
``` rust
125+
// Provides a cursor to the first element of the list
126+
pub fn cursor(&self) -> Cursor<T>;
127+
128+
/// Provides a cursor with mutable references and access to the list
129+
pub fn cursor_mut(&mut self) -> CursorMut<T>;
130+
```
131+
132+
These would provide the following interface:
133+
134+
``` rust
135+
impl<'list, T> Cursor<'list, T> {
136+
/// Move to the subsequent element of the list if it exists or the empty
137+
/// element
138+
pub fn move_next(&mut self);
139+
/// Move to the previous element of the list
140+
pub fn move_prev(&mut self);
141+
142+
/// Get the current element
143+
pub fn current(&self) -> Option<&'list T>;
144+
/// Get the following element
145+
pub fn peek(&self) -> Option<&'list T>;
146+
/// Get the previous element
147+
pub fn peek_before(&self) -> Option<&'list T>;
148+
}
149+
150+
impl<'list T> CursorMut<'list, T> {
151+
/// Move to the subsequent element of the list if it exists or the empty
152+
/// element
153+
pub fn move_next(&mut self);
154+
/// Move to the previous element of the list
155+
pub fn move_prev(&mut self);
156+
157+
/// Get the current element
158+
pub fn current(&mut self) -> Option<&mut T>;
159+
/// Get the next element
160+
pub fn peek(&mut self) -> Option<&mut T>;
161+
/// Get the previous element
162+
pub fn peek_before(&mut self) -> Option<&mut T>;
163+
164+
/// Get an immutable cursor at the current element
165+
pub fn as_cursor<'cm>(&'cm self) -> Cursor<'cm, T>;
166+
167+
// Now the list editing operations
168+
169+
/// Insert `item` after the cursor
170+
pub fn insert(&mut self, item: T);
171+
/// Insert `item` before the cursor
172+
pub fn insert_before(&mut self, item: T);
173+
174+
/// Remove and return the item following the cursor
175+
pub fn pop(&mut self) -> Option<T>;
176+
/// Remove and return the item before the cursor
177+
pub fn pop_before(&mut self) -> Option<T>;
178+
179+
/// Insert `list` between the current element and the next
180+
pub fn insert_list(&mut self, list: LinkedList<T>);
181+
/// Insert `list` between the previous element and current
182+
pub fn insert_list_before(&mut self, list: LinkedList<T>);
183+
184+
/// Split the list in two after the current element
185+
/// The returned list consists of all elements following the current one.
186+
// note: consuming the cursor is not necessary here, but it makes sense
187+
// given the interface
188+
pub fn split(self) -> LinkedList<T>;
189+
/// Split the list in two before the current element
190+
pub fn split_before(self) -> LinkedList<T>;
191+
}
192+
```
193+
One should closely consider the lifetimes in this interface. Both `Cursor` and
194+
`CursorMut` operate on data in their `LinkedList`. This is why, they both hold
195+
the annotation of `'list`.
196+
197+
The lifetime elision for their constructors is correct as
198+
```
199+
pub fn cursor(&self) -> Cursor<T>
200+
```
201+
becomes
202+
```
203+
pub fn cursor<'list>(&'list self) -> Cursor<'list, T>
204+
```
205+
which is what we would expect. (the same goes for `CursorMut`).
206+
207+
Since `Cursor` cannot mutate its list, `current`, `peek` and `peek_before` all
208+
live as long as `'list`. However, in `CursorMut` we must be careful to make
209+
these methods borrow. Otherwise, one could produce multiple mutable references
210+
to the same element.
211+
212+
The only other lifetime annotation is with `as_cursor`. In this case, the
213+
returned `Cursor` must borrow its generating `CursorMut`. Otherwise, it would be
214+
possible to achieve a mutable and immutable reference to the same element at
215+
once.
216+
217+
One question that arises from this interface is what happens if `move_next` is
218+
called when a cursor is on the last element of the list, or is empty (or
219+
`move_prev` and the beginning). A simple way to solve this is to make cursors
220+
wrap around this list back to the empty element. One could complicate the
221+
interface by having move return a `bool`, however this is unnecessary since
222+
`current` is sufficient to know whether the iterator is at the end of the list.
223+
224+
A large consequence of this new interface is that it is a complete superset of
225+
the already existing `Iter` and `IterMut` API. Therefore, the following two
226+
methods added to `IterMut` in the `linked_list_extras` features should be
227+
removed or depreciated:
228+
- `IterMut::insert_next`
229+
- `IterMut::peek_next`
230+
The rest of the iterator methods are stable and should probably stay untouched
231+
(but see below for comments).
232+
233+
# Drawbacks
234+
[drawbacks]: #drawbacks
235+
236+
The cursor interface is rather clunky, and while it allows for efficient code,
237+
it is probably not useful outside of many use-cases.
238+
239+
One of the largest issues with the cursor interface is that it exposes the exact
240+
same interface of iterators (and more), which leads to unnecessary code
241+
duplication.
242+
However, the purpose of iterators seems to be simple, abstract and easy to use
243+
rather than efficient mutation, so cursors and iterators should be used
244+
in different places.
245+
246+
# Rationale and alternatives
247+
[rationale-and-alternatives]: #rationale-and-alternatives
248+
249+
There are several alternatives to this:
250+
251+
1. Implement cursors as a trait extending `Iterator` (see the cursors
252+
pseudo-rfc below)
253+
254+
Since the cursors are just an extension of iterators, it makes some sense to
255+
create them as a trait. However, I see several reasons why this is not the best.
256+
257+
First, cursors work differently than the existing `Iterator` extensions like
258+
`DoubleEndedIterator`. In a `DoubleEndedIterator`, if one calls `next_back` and
259+
then `next`, it should not return the same value, so unlike a cursor, a
260+
`DoubleEndedIterator` does not move back and forth throughout a collection.
261+
262+
Furthermore, while `Iterator` is a general interface for many collections,
263+
`Cursor` is very much specific to linked lists. In other collections such as
264+
`Vec` a cursor does not make sense. So it makes little sense to make a trait
265+
when it will only be used in one place.
266+
267+
2. Using the `IterMut` linked list extensions
268+
269+
Insertion was added to `IterMut` in the `linked_list_extras` feature. Many of
270+
these features could be added to it just as well. But, this overcrowds `IterMut`
271+
with many methods that have nothing to do with iteration (such as deletion,
272+
splitting etc.)
273+
It makes sense to put these explicitly in their own type, and this can be
274+
`CursorMut`.
275+
276+
3. Do not create cursors at all
277+
278+
Everything that cursors do can already be done, albeit in sometimes a less
279+
efficient way. Efficient code can be written by splitting linked lists often,
280+
and while this is a complicated way to do things, the rarity of the use case may
281+
justify keeping things how they are.
282+
283+
# Prior art
284+
[prior-art]: #prior-art
285+
286+
- [cursors pseudo-rfc](https://internals.rust-lang.org/t/pseudo-rfc-cursors-reversible-iterators/386/18)
287+
288+
This rust internals post describes an early attempt at making cursors. The
289+
language was in a different state when it was written (pre-1.0), so details have
290+
changed since then. But this describes several different approaches to making
291+
cursors and where they led.
292+
293+
- Java-style iterators
294+
295+
Java (and other languages) tried to fix this by adding a `remove` function to
296+
their iterators. However, I feel this method would not be the best choice for
297+
Rust (even for specific `IterMut`s like those in LinkedList) since it diverges
298+
from the expected behaviour of iterators.
299+
300+
- [linked list extras issue](https://github.com/rust-lang/rust/issues/27794)
301+
302+
Discussion on the issue tracker about how this is currently managed with
303+
modifications to `IterMut`. The consensus seems to be that it is incomplete, and
304+
it is suggested to create a new `Cursor` and `CursorMut` types.
305+
306+
# Unresolved questions
307+
[unresolved-questions]: #unresolved-questions
308+
309+
- How will this interface interact with iterators?
310+
311+
Will we keep both `Iter` and `Cursor` types? Implement one with another? I feel
312+
like they should be different things, but there is reason to consolidate them.
313+
314+
- Only for linked lists?
315+
316+
Should we implement this for more collections? It could make sense for other
317+
collections, such as trees and arrays, but the design would have to be reworked.

0 commit comments

Comments
 (0)