Collecting NLL examples for a blog post

Hello, [Rust] world!

You may have noticed that we are running a blog post series on the main Rust blog highlighting various nice things about Rust 2018. I'd like to do a post on the new NLL and region checker, and I was thinking it'd be nice if it could draw on "real life" examples.

I'm curious if people have specific examples of code where NLL made your life better (or worse, though I'd probably like to see those in the form of bugs filed on the Rust issue tracker). This might be because of code that used to be a pain but is now easy, or perhaps improved error messages, or just things that you found interesting.

Thanks! :heart:

1 Like

I've been looking for a place to say some nice things about the process of moving from the 2015 to the 2018 edition. Based on my experience with other languages, I booked an afternoon to convert my 6,000 line program. Thanks to rustup and cargo fix, it only took a few minutes. That left me plenty of time to run clippy and make my program Rustier.

3 Likes

I don't have specific blog-post-worthy examples, but I wanted to mention that here in the forum a few new users have asked "why doesn't this example compile!?". I'm very happy I've been able to answer that it does compile now! Thank you for your work on NLL.

2 Likes

I can look for specifics later, but doing Advent of Code this year I've had a few cases with something like:

while let Some(foo) = collection.get_some_ref() {
    if check(foo) {
       collection.mutate(); // thanks NLL!
    }
}
6 Likes

There's this simplification to my interpreter's loop: https://github.com/rpjohnst/dejavu/commit/a3b0981ebdf883ec2166ec6b3219e5235637bcf9#diff-75b762cb6f0a87e32cf77abdf35b436f

It maybe seems more impressive than it is because the match is so long, but I'm happy with it. :slight_smile:

I also have an AoC (Day 8) example. Might be a little long, though:

    pub fn from_reader(reader: BufReader<Box<Read>>) -> Self {
        let mut reader = NodeTreeReader::new(reader);

        let mut node_queue = vec![reader.next_header()];

        let mut nodes = vec![];

        while !node_queue.is_empty() {
            // !!!!! current borrows node_queue here
            let current = node_queue.last_mut().unwrap();
            if current.id.is_none() {
                current.id = Some(nodes.len() as u32);
                nodes.push(Node {
                    children: Vec::with_capacity(current.num_children as usize),
                    metadata: Vec::with_capacity(current.num_metadata as usize),
                });
            }

            if current.num_children > 0 {
                current.num_children -= 1;

                let next_id = nodes.len() as u32;
                nodes[current.id.unwrap() as usize].children.push(next_id);

                // !!!!! but we can still modify node_queue here
                node_queue.push(reader.next_header());
            } else {
                for _ in 0..current.num_metadata {
                    nodes[current.id.unwrap() as usize]
                        .metadata
                        .push(reader.next_int());
                }

                // !!!!! and here!
                node_queue.pop();
            }
        }

        NodeTree { nodes }
}

I do love the error messages:

  1. Take a ref to a here
  2. Take a mut ref to a here
  3. Try to use the first ref here.

Really clear and easy to understand.

3 Likes

While working on Servo's constellation there were quite a few times where we had to add extra scopes in order to please the borrow checker. While this approach worked, NLL let us remove these extra scopes which makes the code more readable.

Commit with these changes:
https://github.com/servo/servo/commit/aecb48d5d1c9bb07fcefae8748d82526aa4ba1c9?w=1

2 Likes

I was going through all kinds of contortions working with hashmaps in structs. The playground example shows the pattern I came up with for the 2018 edition.

    fn test(&mut self, bar: Bar) {
        let qux = self.foo.get_mut(&bar).unwrap();
        qux.qux();
        self.update(&bar, &Qux{});
        let qux = self.foo.get_mut(&bar).unwrap();
        qux.qux();
    }

(I'll worry about performance when it becomes a problem.) With NLL the compiler knows that the first qux is dead before self.update(). Of course, without the second get_mut(), qux is still alive, and I get a borrow error.

Before NLL I could sometimes introduce a dummy scope, but often that was just too awkward. Sometimes I could use Entry, but most of the time I used remove(), worked on the value, and then did an insert(). Even with NLL, I sometimes have to resort to remove()/insert(). Is there something else I should be doing?

Credit goes to my friend jd91mzm2 for coming up with this example to teach me NLL in the Redox Chat a long time ago.

let mut outer = Some(String::new());
if let Some(ref mut inner) = outer {
    // Thing is borrowed mutably, we can mutate it
    inner.push_str("hi");

    // How does this work? Isn't there a mutable reference already?
    // Well, no, it's automatically dropped
    outer = None;

    // can't do this, this is still breaking the borrow checker
    // inner.push('!');
}

NLL was encountered rampantly throughout the development of the Ion system shell, and was also quite common in the distinst Linux distribution installer. This was never a serious issue though, as we were determined, and knew exactly how to dance around the borrow checker to satisfy it.

In most cases, we could avoid the borrow checker by creating temporary values to store state outside of a loop or match, or by manually calling drop on a value and hoping that the borrow checker would understand that the borrow is no longer happening. There were a few scenarios where the unsafe keyword and a raw pointer was required to circumvent the rules entirely, or the decision had to be made to aim for a less-ergonomic API.

Ion

Sadly, most of the NLL comments in Ion were removed for some reason. One of the most common NLL issues within the Ion shell were rampant patterns like the following:

return Some(self.parse_parameter(&self.data[start..self.read - 1].trim()));

These had to be changed to this to compile:

let end = self.read - 1;
return Some(self.parse_array(&self.data[start..end].trim()));

Likewise, the same applied to patterns like so:

self.set("?", self.previous_status.to_string());

Which had the same solution.

Other examples in Ion were a bit more... involved. Figuring out how to handle job expansions, pipelines, and recursive expansions were some of the earliest hurdles.

This entire recent-ish PR is mostly about fixing dependency on the NLL feature so that Ion could build with a stable compiler.

Distinst

Here's a few snippets from distinst, which must retain compatibility for Rust 1.24 from here on out -- at least until 2020. With NLL, unsafe can be avoided entirely, but without it, it's required due to some patterns that cause havoc with borrowck.

// TODO: NLL
let disk = disk as *mut T;

if let Some(partition) = unsafe { &mut *disk }.get_file_system_mut() {
    // TODO: NLL
    let mut found = false;
    if let Some(ref ptarget) = partition.target {
        if ptarget == target {
            found = true;
        }
    }

    if found {
        return Some((path, partition));
    }
}

for partition in unsafe { &mut *disk }.get_partitions_mut() {
    // TODO: NLL
    let mut found = false;
    if let Some(ref ptarget) = partition.target {
        if ptarget == target {
            found = true;
        }
    }

    if found {
        return Some((path, partition));
    }
}

Apparently, because the first match returns a value that is mutably borrowed, the borrow checker refuses to allow the second loop to also perform a mutable borrow, despite the first loop having exited before the second loop began. The inner borrows in these loops also caused issues.

In another section:

let push = match existing_devices
    .iter_mut()
    .find(|d| d.volume_group == lvm.0)
{
    Some(device) => {
        device.add_sectors(partition.get_sectors());
        false
    }
    None => true,
};

if push {
    existing_devices.push(LogicalDevice::new(
        lvm.0.clone(),
        lvm.1.clone(),
        partition.get_sectors(),
        sector_size,
        false,
    ));
}

And another

let mut found = false;

if let Some(ref mut device) = existing_devices
    .iter_mut()
    .find(|d| d.volume_group.as_str() == vg.as_str())
{
    device.add_sectors(partition.get_sectors());
    found = true;
}

if !found {
    existing_devices.push(LogicalDevice::new(
        vg.clone(),
        None,
        partition.get_sectors(),
        sector_size,
        true,
    ));
}
1 Like

Here is a small snippet that is now possible with NLL.
If a key exists in a map, do something to its value, else insert the new key-value pair.

        if let Some(array) = self.nodes.get_mut(&node) {
            array.push(some_value)
        } else {
            self.nodes.insert(node, vec![some_value]);
        }

I know there's a better way to do this using hash_map::Entry, but for those who don't know about it the above feels like a logical approach to the problem, and it was frustrating to see it fail pre-NLL.

1 Like