Custom Types II

From Ryan & Thea

After playing around with a Node struct on Monday, we worked together to define a linked list. I think this exercise is useful for practicing some of the ownership and reference concepts we’ve encountered, and it introduces some light object oriented programming.

Below is a recap of what we covered and a little bit extra (in case you want some more practice).

Defining the struct

We started by defining a LinkedList struct:

struct LinkedList {
    head: Option<Box<Node>>, 
    length: usize, 
}

I argued that we want an Option for the head of the list, because we want it to be None if the list is empty. I used a Box here just for consistency – all of the other nodes are going to be allocated on the heap (because of the way we defined struct Node last lecture), and it doesn’t make sense to me for the first node to be any different.

I’m using a usize for the length; this is a large unsigned integer type that’s commonly used to represent the size of things.

The impl block

To implement methods associated with this Linked List, we put them in an impl block:

impl LinkedList {
    // Methods for the linked list here. 
}

For example, if you were implementing a rectangle, you might want to have a constructor to initialize the rectangle with some width and height, a function to return the rectangle’s area, etc.

This is kind of similar to implementing methods for a class in C++, Python, etc.

new()

We started off with a constructor – a function to create and return a new LinkedList. It’s conventional in Rust to name a constructor new:

impl LinkedList {
    fn new() -> LinkedList {
        LinkedList { head: None, length: 0 } // returns
    }
}

We can then invoke this to create and return a new, empty LinkedList object like this:

fn main() { 
    let l1 = LinkedList::new();
}

len()

One simple thing we can do is get the length of our linked list:

fn main() { 
    let l1 = LinkedList::new();
    println!("Length: {}", l1.len()); // should be 0
}

One natural thing to try might be this:

// Won't compile
fn len() -> usize {
    length
}

This won’t work. The Rust compiler will look at this code and think: length of what?

Unlike new(), len() needs a specific linked list object to operate on. (When we call l1.len(), we want “the length of l1”.)

To make this happen, we make the first parameter self: the Rust compiler will interpret this as “the object this function is applied to.” (This is similar to Python, and it’s different from C++.)

// Compiles!
fn len(&self) -> usize {
    length
}

If we call l1.len() in main, as above, a reference to l1 will implicitly get passed to the parameter &self.

Note that I passed in a reference, &self, above. If I passed in self without the &, then ownership of the linked list in quest would be transferred into the scope of len. That means that when len returned, the list would be destroyed, and the caller couldn’t use it anymore. It’s not super practical to have a method that gives you the length of a data structure and then immediately destroys the data structure. That would be silly.

front()

Let’s implement a method that returns a reference to the first node in the list (if there is one). This seems like a suitable signature:

fn front(&self) -> Option<&Box<Node>> {
}

If our list has no nodes, this should return None; otherwise, it should return Some with a (here, immutable) reference to the first node in the list. Since we’re trying to deal with the different possibilities/“variants” of Option, a match expression seems appropriate. Here’s attempt 1:

/* Won't compile. */
fn front(&self) -> Option<&Box<Node>> {
    match self.head {
        Some(node) => Some(node),
        None => None,
    }
}

This code won’t compile. It’ll give us an error like this:

Some(node) => Some(node),
             ^^^^
             |
             expected `&Box<Node>`, found struct `Box`
            help: consider borrowing here: `&node`

   = note: expected reference `&Box<Node>`
                 found struct `Box<Node>`

Let’s interpret:

We don’t actually want ownership – we want a reference! To fix this, we can match on &self.head instead. This signals to the compiler that we are interested in referencing the stuff inside of the Option instead of taking ownership. (Note: There’s a little bit of syntax magic happening here, which is relatively rare for the Rust language; you can read about the design decision here if you’re interested.)

// Compiles!
fn front(&self) -> Option<&Box<Node>> {
    match &self.head {
        Some(node) => Some(node),
        None => None,
    }
}

This works!

You might recall from last lecture that this is really what Option::as_ref() is doing: if you have a reference to an option (e.g. &self.head), it will create a new Option containing a reference to whatever was inside the option. It turns out that as_ref is implemented using a nearly identical match expression to what we just wrote.

// Also compiles! (Equivalent to above.)
fn front(&self) -> Option<&Box<Node>> {
    self.head.as_ref()
}

Note that we don’t need (&self.head).as_ref() here; self was passed as a reference.

I don’t care that much about you understanding and using as_ref(). What I care about is that you are able to reason about the references and types involved – e.g., what’s the difference between a “reference to an Option” and an “Option with a reference to a “node”? If you can look at the illustration towards the end of lecture 6 (additional notes here) and get a basic idea of what’s going on, then I’m happy.

back()

Let’s try getting a reference to the last element! This one is a little more tricky, since we need to iterate through the list until we reach the end.

We showed how to iterate through the list last time:

fn back(&self) -> Option<&Box<Node>> {
    let mut curr_opt = self.front();
    while curr_opt.is_some() {
        let curr_node = curr_opt.unwrap();
        // Go to next node
        curr_opt = curr_node.next.as_ref();
    }
}

We want to return a reference to the last node in the list, which will be the first node where we see curr_node.next be None. So, we might try something like this:

// Almost works
fn back(&self) -> Option<&Box<Node>> {
    let mut curr_opt = self.front();
    while curr_opt.is_some() {
        let curr_node = curr_opt.unwrap();
        if curr_node.next.is_none() {
            // Explicit use of `return` keyword here, 
            // since we're returning early. 
            return Some(curr_node);
        }
        // Go to next node
        curr_opt = curr_node.next.as_ref();
    }
}

This nearly works, but the compiler warns us that we’ve forgotten to handle the case where our while loop doesn’t run at all. In that case, the list is empty, so we should return None.

fn back(&self) -> Option<&Box<Node>> {
    let mut curr_opt = self.front();
    while curr_opt.is_some() {
        let curr_node = curr_opt.unwrap();
        if curr_node.next.is_none() {
            return Some(curr_node);
        }
        // Go to next node
        curr_opt = curr_node.next.as_ref();
    }
    // If the loop above didn't run, the list was empty. 
    // Return None.
    None
}

front_mut()

Note that the front() and back() methods above return immutable references to Nodes. In some cases, we might want mutable references to these nodes – maybe as helper functions for an internal insert method or to allow a caller to change a value without creating a new node.

We can adapt front to return the Option of a mutable reference:

fn front_mut(&mut self) -> Option<&mut Box<Node>> {
}

Our goal here is similar to front(): we want to create a new Option, and we want that option to store a reference – here, a mutable reference – to the Box<Node> at self.head.

It turns out that there’s a function in Rust called as_mut(), which does the exact same thing as as_ref(), except with mutable references.

fn front_mut(&mut self) -> Option<&mut Box<Node>> {
    self.head.as_mut()
}

Again, I don’t care that much about you understanding the details or using as_mut(). What I care about is that you’re able to reason about the difference between things like an Option<&Box<Node>> and an Option<&mut Box<Node>>; &mut Option<Box<Node>> and Option<&mut Box<Node>>; and front and front_mut.

push_front()

OPTIONAL AND MORE ADVANCED. I’m including this here for some extra practice, but it’s probably trickier than what you’ll be expected to do, and you should feel free to skip it.

Let’s implement a method to add an element!

We want our function signature to look something like this:

fn push_front(&mut self, val: i32) {
}

self needs to be mutable, because we’re going to modify the list, and it needs to be a reference because we don’t want to transfer ownership here.

First, we probably should create the node we want to add, and we’ll eventually want to increment the length.

fn push_front(&mut self, val: i32) {
    let node = Some(Box::new( Node { value: val, next: None } ));
    self.length = self.length + 1;
    /* TODO: Insert node */
}

The next part is tricky, and it took me a few tries to get it right. What we want to do is something like this:

One natural thing to try might be this:

// Won't compile
fn push_front(&mut self, val: i32) {
    let mut node = Box::new( Node { value: val, next: None } );
    self.length = self.length + 1;
    node.next = self.head;
    self.head = Some(node);
}

This won’t compile. We’ll get an errror like this: cannot move out of self.head, which is behind a mutable reference.

This is saying that self.head currently owns some object, and we’re trying to move ownership of that object into a different variable – specifically, node, via its node.next member.

The Rust compiler won’t let us do that; the scope of this function doesn’t have ownership of the linked list it’s operating on; it just has a reference. If it doesn’t have ownership of the linked list, including the Option in self.head, it can’t transfer ownership somewhere else.

In order to fix this, we need to use a new function that we haven’t seen yet: std::mem::replace.

fn push_front(&mut self, val: i32) {
    let node = Some(Box::new( Node { value: val, next: None } ));
    self.length = self.length + 1;
    let old_head = std::mem::replace(&mut self.head, node);
    // TODO: point self.head to old_head
}

This code will

(As with as_ref() and as_mut(), I don’t really care about you memorizing how to use mem::replace; I want you to be able to reason, conceptually, about how it’s moving around ownership.)

Finally, we need to point self.head to old_head:

// Won't compile
fn push_front(&mut self, val: i32) {
    let node = Some(Box::new( Node { value: val, next: None } ));
    self.length = self.length + 1;
    let old_head = std::mem::replace(&mut self.head, node);
    self.head.as_mut().unwrap().next = old_head;
}

Breaking this down a bit:

Fin.

Whew! That was a lot. In case you want to play around with the code, I’ve pasted everything below (along with some basic tests) so that you can copy and paste it into your own editor:

struct Node {
    value: i32, 
    next: Option<Box<Node>>,
}

impl Node {
    fn next(&self) -> Option<&Box<Node>> {
        self.next.as_ref()
    }
}

struct LinkedList {
    head: Option<Box<Node>>, 
    length: usize, 
}

impl LinkedList {
    fn new() -> LinkedList {
        /* return */
        LinkedList { head: None, length: 0 }
    }

    fn len(&self) -> usize {
        /* return */
        self.length
    }

    fn front(&self) -> Option<&Box<Node>> {
        /* return */
        self.head.as_ref()
    }

    fn back(&self) -> Option<&Box<Node>> {
        let mut curr_opt = self.front();
        while curr_opt.is_some() {
            let curr_node = curr_opt.unwrap();
            if curr_node.next.is_none() {
                return Some(curr_node);
            }
            curr_opt = curr_node.next.as_ref();
        }
        None
    }

    fn front_mut(&mut self) -> Option<&mut Box<Node>> {
        self.head.as_mut()
    }

    fn push_front(&mut self, val: i32) {
        let node = Some(Box::new( Node { value: val, next: None } ));
        self.length = self.length + 1;
        let old_head = std::mem::replace(&mut self.head, node);
        self.head.as_mut().unwrap().next = old_head;
        let tmp = self.head.as_mut();
    }
}

fn main() { 
    let mut l1 = LinkedList::new();
    println!("{}", l1.len());
    l1.push_front(1);
    println!("Front: {}", l1.front().unwrap().value);
    println!("Back: {}", l1.back().unwrap().value);

    l1.push_front(2);
    println!("After adding 2:");
    println!("Front: {}", l1.front().unwrap().value);
    println!("Back: {}", l1.back().unwrap().value);

    let node_mut = l1.front_mut();
    node_mut.unwrap().value = 3;
    println!("After changing to 3:");
    println!("Front: {}", l1.front().unwrap().value);
    println!("Back: {}", l1.back().unwrap().value);

    let node_using_next = l1.front().unwrap().next().unwrap();
    println!("Testing the `next` function on Node! Second element: {}", node_using_next.value);

    println!("Length after adding: {}", l1.len());
}