for_each, map, and compiler warnings

Rust has a great attribute you can add to structs called must_use. It is used on the Result<T, E> enum in the standard library, which looks like this:


#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Debug, Hash)]
#[must_use = "this `Result` may be an `Err` variant, which should be handled"]
#[stable(feature = "rust1", since = "1.0.0")]
pub enum Result<T, E> {
    /// Contains the success value
    #[stable(feature = "rust1", since = "1.0.0")]
    Ok(#[stable(feature = "rust1", since = "1.0.0")] T),

    /// Contains the error value
    #[stable(feature = "rust1", since = "1.0.0")]
    Err(#[stable(feature = "rust1", since = "1.0.0")] E),
}

And on std::iter::Map, it looks like this:

#[must_use = "iterator adaptors are lazy and do nothing unless consumed"]
pub struct Map<I, F> { /* fields omitted */ }

So what does this attribute do? From the docs:

must_use – on structs and enums, will warn if a value of this type isn’t used or assigned to a variable. You may also include an optional message by using #[must_use = "message"] which will be given alongside the warning.

In other words, if I call a function that returns a type with must_use, then I will get a compiler warning unless I assign the struct to a variable or perform some operation on it. This helpful type of warning really helped me when I was writing some example code the other day.

As a little background, I’ve been doing a lot of Ruby and JavaScript at work locally, so when I started writing example code in Rust, I was thinking about JavaScript’s map and forEach, not Rust’s.

So when I was writing some rust example code, I wrote:

    let mut my_collection = SomeCollection::new();

    (0..10).map( |item| {
        let value = format!("value number {}", item + 1);
        my_collection.insert(value);
    });

instead of:

    let mut my_collection = SomeCollection::new();

    (0..10).for_each( |item| {
        let value = format!("value number {}", item + 1);
        my_collection.insert(value);
    });

See the difference? I didn’t, because I was thinking that the only difference between map and for_each was returning the changed collection. Using map to iterate a collection in JavaScript is fairly common, so it didn’t occur to me that these methods are quite different in Rust.

Then, I got this very helpful compiler warning:

   Compiling playground v0.0.1 (file:///playground)
warning: unused `std::iter::Map` which must be used: iterator 
adaptors are lazy and do nothing unless consumed
  --> src/main.rs:22:5
   |
22 | /     (0..10).map( |item| {
23 | |         let value = format!("value number {}", item + 1);
24 | |         my_collection.insert(value);
25 | |     });
   | |_______^
   |
   = note: #[warn(unused_must_use)] on by default

My call to map wasn’t going to do anything at all in this context. my_collection was empty at the end of the first example. The iterator returned by map was waiting for something to evaluate it, but hadn’t run the closure I passed in yet.

This laziness in iterator adapters reminds me of C#’s LINQ queries, where operations like map and filter are lazy, and it’s common to see ToList() or ToArray() calls on the end of them when they need to be evaluated.

That’s it for me today – I just wanted to share how happy I am that Rust has such helpful compiler warnings. I hope that more languages will follow suit, and that more people will consider Rust.

Till next time, happy learning!

-Will

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s