% Layout

So what's a singly-linked queue like? Well, when we had a singly linked stack we pushed onto one end of the list, and then popped off the same end. The only difference between a stack and a queue is that a queue pops off the other end. So from our stack implementation we have:

input list:
[Some(ptr)] -> (A, Some(ptr)) -> (B, None)

stack push X:
[Some(ptr)] -> (X, Some(ptr)) -> (A, Some(ptr)) -> (B, None)

stack pop:
[Some(ptr)] -> (A, Some(ptr)) -> (B, None)

To make a queue, we just need to decide which operation to move to the end of the list: push, or pop? Since our list is singly-linked, we can actually move either operation to the end with the same amount of effort.

To move push to the end, we just walk all the way to the None and set it to Some with the new element.

input list:
[Some(ptr)] -> (A, Some(ptr)) -> (B, None)

flipped push X:
[Some(ptr)] -> (A, Some(ptr)) -> (B, Some(ptr)) -> (X, None)

To move pop to the end, we just walk all the way to the node before the None, and take it:

input list:
[Some(ptr)] -> (A, Some(ptr)) -> (B, Some(ptr)) -> (X, None)

flipped pop:
[Some(ptr)] -> (A, Some(ptr)) -> (B, None)

We could do this today and call it quits, but that would stink! Both of these operations walk over the entire list. Some would argue that such a queue implementation is indeed a queue because it exposes the right interface. However I believe that performance guarantees are part of the interface. I don't care about precise assymptotic bounds, but rather "fast" and "slow". Queues guarantee that push and pop are fast, and walking over the whole list is definitely not fast.

One key observation is that we're wasting a ton of work doing the same thing over and over. Can we memoize this work? Why, yes! We can store a pointer to the end of the list, and just jump straight to there!

It turns out that only one inversion of push and pop works with this. Because our list is singly-linked, we can't effeciently walk backwards in the list. To invert pop we would have to move the "tail" pointer backwards. But if we instead invert push we only have to move the "head" pointer forwards, which is easy.

Let's try that:

use std::mem;
# fn main() {}

pub struct List<T> {
    head: Link<T>,
    tail: Link<T>, // NEW!
}

type Link<T> = Option<Box<Node<T>>>;

struct Node<T> {
    elem: T,
    next: Link<T>,
}

impl<T> List<T> {
    pub fn new() -> Self {
        List { head: None, tail: None }
    }

    pub fn push(&mut self, elem: T) {
        let new_tail = Box::new(Node {
            elem: elem,
            // When you push onto the tail, your next is always None
            next: None,
        });

        // swap the old tail to point to the new tail
        let old_tail = mem::replace(&mut self.tail, Some(new_tail));

        match old_tail {
            Some(mut old_tail) => {
                // If the old tail existed, update it to point to the new tail
                old_tail.next = Some(new_tail);
            }
            None => {
                // Otherwise, update the head to point to it
                self.head = Some(new_tail);
            }
        }
    }
}

I'm going a bit faster with the impl details now since we should be pretty comfortable with this sort of thing. Not that you should necessarily expect to produce this code on the first try. I'm just skipping over some of the trail-and-error we've had to deal with before. I actually made a ton of mistakes writing this code that I'm not showing. You can only see me leave off a mut or ; so many times before it stops being instructive. Don't worry, we'll see plenty of other error messages!

> cargo build
   Compiling lists v0.1.0 (file:///Users/ABeingessner/dev/too-many-lists/lists)
src/fifth.rs:33:38: 33:46 error: use of moved value: `new_tail` [E0382]
src/fifth.rs:33                 old_tail.next = Some(new_tail);
                                                     ^~~~~~~~
src/fifth.rs:28:58: 28:66 note: `new_tail` moved here because it has type `Box<fifth::Node<T>>`, which is non-copyable
src/fifth.rs:28         let old_tail = mem::replace(&mut self.tail, Some(new_tail));
                                                                         ^~~~~~~~
src/fifth.rs:37:34: 37:42 error: use of moved value: `new_tail` [E0382]
src/fifth.rs:37                 self.head = Some(new_tail);
                                                 ^~~~~~~~
src/fifth.rs:28:58: 28:66 note: `new_tail` moved here because it has type `Box<fifth::Node<T>>`, which is non-copyable
src/fifth.rs:28         let old_tail = mem::replace(&mut self.tail, Some(new_tail));
                                                                         ^~~~~~~~
error: aborting due to 2 previous errors
Could not compile `lists`.

Shoot!

use of moved value: new_tail

Box doesn't implement Copy, so we can't just assign it to two locations. More importantly, Box owns the thing it points to, and will try to free it when it's dropped. If our push implementation compiled, we'd double-free the tail of our list! Actually, as written, our code would free the old_tail on every push. Yikes! 🙀

Alright, well we know how to make a non-owning pointer. That's just a reference!

# fn main() {}
pub struct List<T> {
    head: Link<T>,
    tail: Option<&mut Node<T>>, // NEW!
}

type Link<T> = Option<Box<Node<T>>>;

struct Node<T> {
    elem: T,
    next: Link<T>,
}

impl<T> List<T> {
    pub fn new() -> Self {
        List { head: None, tail: None }
    }

    pub fn push(&mut self, elem: T) {
        let new_tail = Box::new(Node {
            elem: elem,
            // When you push onto the tail, your next is always None
            next: None,
        });

        // Put the box in the right place, and then grab a reference to its Node
        let new_tail = match self.tail.take() {
            Some(old_tail) => {
                // If the old tail existed, update it to point to the new tail
                old_tail.next = Some(new_tail);
                old_tail.next.as_mut().map(|node| &mut **node)
            }
            None => {
                // Otherwise, update the head to point to it
                self.head = Some(new_tail);
                self.head.as_mut().map(|node| &mut **node)
            }
        };

        self.tail = new_tail;
    }
}

Nothing too tricky here. Same basic idea as the previous code, except we're using some of that implicit return goodness to extract the tail reference from wherever we stuff the actual Box.

> cargo build
   Compiling lists v0.1.0 (file:///Users/ABeingessner/dev/too-many-lists/lists)
src/fifth.rs:3:18: 3:30 error: missing lifetime specifier [E0106]
src/fifth.rs:3     tail: Option<&mut Node<T>>, // NEW!
                                ^~~~~~~~~~~~
src/fifth.rs:3:18: 3:30 help: run `rustc --explain E0106` to see a detailed explanation
error: aborting due to previous error
Could not compile `lists`.

Oh right, we need to give references in types lifetimes. Hmm... what's the lifetime of this reference? Well, this seems like IterMut, right? Let's try what we did for IterMut, and just add a generic 'a:

# fn main() {}
pub struct List<'a, T: 'a> {
    head: Link<T>,
    tail: Option<&'a mut Node<T>>, // NEW!
}

type Link<T> = Option<Box<Node<T>>>;

struct Node<T> {
    elem: T,
    next: Link<T>,
}

impl<'a, T> List<'a, T> {
    pub fn new() -> Self {
        List { head: None, tail: None }
    }

    pub fn push(&mut self, elem: T) {
        let new_tail = Box::new(Node {
            elem: elem,
            // When you push onto the tail, your next is always None
            next: None,
        });

        // Put the box in the right place, and then grab a reference to its Node
        let new_tail = match self.tail.take() {
            Some(old_tail) => {
                // If the old tail existed, update it to point to the new tail
                old_tail.next = Some(new_tail);
                old_tail.next.as_mut().map(|node| &mut **node)
            }
            None => {
                // Otherwise, update the head to point to it
                self.head = Some(new_tail);
                self.head.as_mut().map(|node| &mut **node)
            }
        };

        self.tail = new_tail;
    }
}
cargo build
   Compiling lists v0.1.0 (file:///Users/ABeingessner/dev/too-many-lists/lists)
src/fifth.rs:35:27: 35:35 error: cannot infer an appropriate lifetime for autoref due to conflicting requirements
src/fifth.rs:35                 self.head.as_mut().map(|node| &mut **node)
                                          ^~~~~~~~
src/fifth.rs:18:5: 40:6 help: consider using an explicit lifetime parameter as shown: fn push(&'a mut self, elem: T)
src/fifth.rs:18     pub fn push(&mut self, elem: T) {
src/fifth.rs:19         let new_tail = Box::new(Node {
src/fifth.rs:20             elem: elem,
src/fifth.rs:21             // When you push onto the tail, your next is always None
src/fifth.rs:22             next: None,
src/fifth.rs:23         });
                ...
error: aborting due to previous error

Oh lord. When the compiler starts telling us to just start adding lifetimes in random places, it's a red flag that the compiler is deeply confused. But uh... ok let's try that I guess?

    pub fn push(&'a mut self, elem: T) {
cargo build
   Compiling lists v0.1.0 (file:///Users/ABeingessner/dev/too-many-lists/lists)
src/fifth.rs:9:5: 9:12 warning: struct field is never used: `elem`, #[warn(dead_code)] on by default
src/fifth.rs:9     elem: T,
                   ^~~~~~~

Oh, hey, that worked! Great!

Let's just do pop too:

pub fn pop(&'a mut self) -> Option<T> {
    // Grab the list's current head
    self.head.take().map(|head| {
        let head = *head;
        self.head = head.next;

        // If we're out of `head`, make sure to set the tail to `None`.
        if self.head.is_none() {
            self.tail = None;
        }

        head.elem
    })
}

Let's try to just write a quick test for that:

mod test {
    use super::List;
    #[test]
    fn basics() {
        let mut list = List::new();

        // Check empty list behaves right
        assert_eq!(list.pop(), None);

        // Populate list
        list.push(1);
        list.push(2);
        list.push(3);

        // Check normal removal
        assert_eq!(list.pop(), Some(1));
        assert_eq!(list.pop(), Some(2));

        // Push some more just to make sure nothing's corrupted
        list.push(4);
        list.push(5);

        // Check normal removal
        assert_eq!(list.pop(), Some(3));
        assert_eq!(list.pop(), Some(4));

        // Check exhaustion
        assert_eq!(list.pop(), Some(5));
        assert_eq!(list.pop(), None);
    }
}
cargo test
   Compiling lists v0.1.0 (file:///Users/ABeingessner/dev/too-many-lists/lists)
src/fifth.rs:68:9: 68:13 error: cannot borrow `list` as mutable more than once at a time
src/fifth.rs:68         list.push(2);
                        ^~~~
src/fifth.rs:66:9: 66:13 note: previous borrow of `list` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `list` until the borrow ends
src/fifth.rs:66         list.push(1);
                        ^~~~
src/fifth.rs:70:6: 70:6 note: previous borrow ends here
src/fifth.rs:59     fn basics() {
...
src/fifth.rs:70     }
                    ^


**NOT SHOWN: LITERALLY A THOUSAND LINES OF BORROW CHECK ERRORS**


src/fifth.rs:84:20: 84:24 error: cannot borrow `list` as mutable more than once at a time
src/fifth.rs:84         assert_eq!(list.pop(), None);
                                   ^~~~
<std macros>:1:1: 9:39 note: in expansion of assert_eq!
src/fifth.rs:84:9: 84:38 note: expansion site
src/fifth.rs:83:20: 83:24 note: previous borrow of `list` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `list` until the borrow ends
src/fifth.rs:83         assert_eq!(list.pop(), Some(1));
                                   ^~~~
<std macros>:1:1: 9:39 note: in expansion of assert_eq!
src/fifth.rs:83:9: 83:41 note: expansion site
src/fifth.rs:85:6: 85:6 note: previous borrow ends here
src/fifth.rs:59     fn basics() {
...
src/fifth.rs:85     }
                    ^
error: aborting due to 66 previous errors

🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀🙀

OH MY GEEZ WHAT.

66 borrow check errors.

Oh my goodness.

I'm pretty sure we just hit this bug in the compiler.

But the compiler's not wrong for vomiting all over us. We just committed a cardinal Rust sin: we stored a reference to ourselves inside ourselves. Somehow, we managed to convince Rust that this totally made sense in our push and pop implementations (I was legitimately shocked we did). I believe the reason is that Rust can't yet tell that the reference is into ourselves from just push and pop -- or rather, Rust doesn't really have that notion at all. Reference-into-yourself falls over as an emergent behaviour.

However as soon as we tried to use our list, everything quickly fell apart. When we call push or pop, we promptly store a reference to ourselves in ourselves and become trapped. We are literally borrowing ourselves.

Our pop implementation hints at why this could be really dangerous:

// ...
if self.head.is_none() {
    self.tail = None;
}

What if we forgot to do this? Then our tail would point to some node that had been removed from the list. Such a node would be instantly freed, and we'd have a dangling pointer which Rust was supposed to protect us from!

And indeed Rust is protecting us from that kind of danger. Just in a very... roundabout way.

So what can we do? Go back to Rc<RefCell>> hell?

Please. No.

No instead we're going to go off the rails and use raw pointers. Our layout is going to look like this:

# fn main() {}
pub struct List<T> {
    head: Link<T>,
    tail: *mut Node<T>, // DANGER DANGER
}

type Link<T> = Option<Box<Node<T>>>;

struct Node<T> {
    elem: T,
    next: Link<T>,
}

And that's that. None of this wimpy reference-counted-dynamic-borrow-checking nonsense! Real. Hard. Unchecked. Pointers.

Let's be C everyone. Let's be C all day.

I'm home. I'm ready.

Hello unsafe.

results matching ""

    No results matching ""