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:
self.head
is anOption
containing (and owning!) aBox<Node>
.- When we use a
match
expression here, it effectivelyunwrap
s theOption
and stores the result of unwrapping in the variablenode
. - This transfers ownership to the variable
node
.
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()
}
- In
front
, theas_ref()
call:- takes in an
&Option<Box<Node>>
(note immutability), - creates a new
Option
, - and stores a
&Box<Node>
in thatOption
. - So we get back an
Option<&Box<Node>>
. Note the immutability of this reference contained in the Option.
- takes in an
- In
front_mut
, theas_mut()
call:- takes in a
&mut Option<Box<Node>>
(note mutability), - creates a new
Option
, - and stores in that
Option
a&mut Option<Box<Node>>
. - So we get back:
Option<&mut Box<Node>>
. Note the mutability of this reference contained in the option.
- takes in a
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:
- Set this new node’s
next
to beself.head
- Set
self.head
equal to this newnode
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
.
- This function takes in two parameters: a destination and a source. The destination should be a mutable reference to an object of the same type as
src
. - It then moves
src
into the referenceddest
, - And it returns the previous
dest
value. - Neither value is dropped (destroyed).
- The
std::mem::
means that this is from the Rust standard (std
) library in themem
package.
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
- Take the object
node
– anOption<Box<Node>>
– and giveself.head
ownership of it. - Take the object that used to be owned by
self.head
– also anOption<Box<Node>>
– and return it, giving ownership to the local variableold_head
.
(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:
- We can use
self.head
to get a mutable reference to what’s stored in thehead
member of our linked list, which is anOption
(more specifically, anOption<Box<Node>>
). - We use
as_mut
to instead get a newOption
– which we own – that contains a&mut Box<Node>
. We now have anOption<&mut Box<Node>>
. The containedBox<Node>
represents the first node in the linked list. - We then
unwrap()
theOption
. Callingunwrap()
will destroy theOption
we just created and give us the&mut Box<Node>
inside of it.- (I feel fine about unwrapping here; since we just added a node, I feel confident that
self.head
won’t beNone
.)
- (I feel fine about unwrapping here; since we just added a node, I feel confident that
- Finally, we can access
next
directly, and set it toold_head
.
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());
}