Search code examples
algorithmrustlinked-list

Delete nodes in singly linked list (two implementations)


I have two implementation for removal function. Method 1 does not work because the compiler said curr is borrowed.

What is the difference between while let Some(boxed_node) = curr and match curr?

Method 1

fn delete_method_1(&mut self, val: i32) {
    let mut curr = &mut self.head;

    while let Some(boxed_node) = curr {
        if boxed_node.val == val {
            *curr = boxed_node.next.take(); // it does not work
        } else {
            curr = &mut boxed_node.next
        }
    }
}
Error message of Method 1
while let Some(boxed_node) = curr {
   |                        ---------- `*curr` is borrowed here
26 |             if boxed_node.val == val {
27 |                 *curr = boxed_node.next.take(); // it does not work
   |                 ^^^^^
   |                 |
   |                 `*curr` is assigned to here but it was already borrowed
   |                 borrow later used here

Method 2

fn delete_method_2(&mut self, val: i32) {
    let mut curr = &mut self.head;
    loop {
        match curr {
            None => return,
            Some(boxed_node) if boxed_node.val == val => {
                *curr = boxed_node.next.take();
            },
            Some(boxed_node) => {
                curr = &mut boxed_node.next;
            }
        }
    }
}

I expected Method 1 to work.


Solution

  • This is actually not really a difference between while let and match. If you wrote the second method to be exactly like the first it would look like this:

    fn delete_method_2(&mut self, val: i32) {
        let mut curr = &mut self.head;
    
        loop {
            match curr {
                None => return,
                Some(boxed_node) => {
                    if boxed_node.val == val {
                        *curr = boxed_node.next.take();
                    } else {
                        curr = &mut boxed_node.next;
                    }
                }
            }
        }
    }
    

    And you'd get the exact same error.

    What's actually happening is a little bit subtle. First of all, match ergonomics will dereference match scrutinees for you automatically. What's actuall happening is this:

    fn delete_method_2(&mut self, val: i32) {
        let mut curr = &mut self.head;
    
        loop {
            //    v Rust inserts this dereference...
            match *curr {
                None => return,
                //   vvvvvvv and this borrow automatically
                Some(ref mut boxed_node) => {
                    if boxed_node.val == val {
                        *curr = boxed_node.next.take();
                    } else {
                        curr = &mut boxed_node.next;
                    }
    
                }
            }
        }
    }
    

    The while let version looks pretty much the same. The problem here is that the line curr = &mut boxed_node.next forces boxed_node to be borrowed for the lifetime of curr. With fake lifetime annotations:

    fn delete_method_2(&mut self, val: i32) {
        let mut curr = &'curr mut self.head;
    
        loop {
            match *curr {
                None => return,
                Some(ref 'curr mut boxed_node) => {
                    if boxed_node.val == val {
                        *curr = boxed_node.next.take();
                    } else {
                        //            we force the compiler to match this lifetime
                        //      vvvvv exactly
                        curr = &'curr mut boxed_node.next;
                    }
    
                }
            }
        }
    }
    

    So because boxed_node has to be borrowed for 'curr, we're not allowed to assign to *curr within that same scope. The borrow checker can't (yet) figure out that the two branches need to borrow boxed_node for different lifetimes.

    When you separate the cases into two distinct match arms, however, the compiler can figure out the appropriate lifetimes to borrow boxed_node for in each case:

    fn delete_method_2(&mut self, val: i32) {
        let mut curr = &'curr mut self.head;
    
        loop {
            match *curr {
                None => return,
                Some(ref '_ mut boxed_node) if boxed_node.val == val => {
                    *curr = boxed_node.next.take();
                }
                Some(ref 'curr mut boxed_node) => {
                    curr = &'curr mut boxed_node.next;
                }
            }
        }
    }
    

    In the first case, the compiler just needs to borrow boxed_node for the duration of the take call, after which curr isn't considered borrowed anymore and we can re-assign it.

    In the second case, the compiler can borrow boxed_node for the 'curr lifetime.

    There are probably a myriad technical mistakes in that explanation, but big picture I think that's what's going on.