Rust Tips and Tricks (14 min. read)

Posted on — shared on Hacker News Twitter Reddit

Rust has been seeing increased adoption, both in academia and industry, over the past few months. This is great news for the language and its community. However, it inevitably also means that a number of people with relatively little experience in Rust are exposed to Rust codebases, and, in many cases, are asked to modified them.

As programmers, when we start using a new language, we often carry over the isms of the languages we already know into our new code. We don’t know how to write idiomatic code, and don’t know the many convenient shortcuts the language provides, but we make do and fiddle with things until the program compiles and runs.

This is perfectly natural. Over time, by seeing other people’s code, we learn, we adapt, and we write better code. This post tries to speed up that process by showing you some of the Rust shorthands I have discovered so far.

Returning values

In Rust, functions, conditionals, matches, and blocks all automatically return the value of their last expression. Thus, instead of this:

impl Bar {
    fn get_baz(&self) -> u64 {
        return self.baz;
    }
}

You can use:

impl Bar {
    fn get_baz(&self) -> u64 {
        self.baz
    }
}

This is particularly useful for closures:

let words = vec!["hello", "world"];
let word_len = words.into_iter().map(|w| {
    return w.len();
}).collect();

Can be rewritten as

let words = vec!["hello", "world"];
let word_len = words.into_iter().map(|w| w.len()).collect();

In fact, you can get rid of the closure altogether, and pass the str::len method directly to map instead:

let words = vec!["hello", "world"];
let word_len = words.into_iter().map(str::len).collect();

Things that can’t happen (yet)

While writing code, you often reach an edge case (or maybe even a common code path) that you aren’t ready to deal with just yet. In other languages, you might just write return 0, exit(1), assert(false), or something similar. In Rust, there is a standard way to deal with this case: unimplemented!(). This function (well, macro) halts your program when invoked, prints a stack trace saying “reached unimplemented code”, and points you directly to the line in question.

Similarly, if you have a conditional that simply shouldn’t be reachable, you can use the semantically appropriate unreachable!(), which has a similar effect as calling unimplemented!().

Taming Options

Imagine someone just passed you an Option that you know is Some, and you want to look at what’s inside it. Easy, you say, all the tutorials told me that I can just use unwrap():

struct Bar { opt: Option<String> };
impl Bar {
    fn baz(&self) {
	let s = self.opt.unwrap();
    }
}
error[E0507]: cannot move out of borrowed content
 --> x.rs:5:17
  |
5 |         let s = self.opt.unwrap();
  |                 ^^^^ cannot move out of borrowed content

Damn. You are only borrowing the Option (i.e., you have an &Option), so you can’t call unwrap() (which moves the value out of the option). Fine fine, you say. I’ll use unreachable!() like you taught me along with the neat if let single pattern matching construct that Rust provides:

impl Bar {
    fn baz(&self) {
        if let Some(ref s) = self.opt {
	    // do something with s
	} else {
	    unreachable!();
	}
    }
}

That compiles, but it doesn’t look very nice, and you’re left with an extra level of indentation. Isn’t there a better way? I’m glad you asked:

impl Bar {
    fn baz(&self) {
	let s = self.opt.as_ref().unwrap();
    }
}

The magical method as_ref() has the helpful signature &Option<T> -> Option<&T>. That is, it turns a reference to an Option into an Option that holds a reference. Perfect!

Updating or inserting into a map

If you write code that’s anything like what I write, you use maps all over the place. HashMaps in particular. If you’re filling the map, you might write code like:

let mut map: HashMap<_, Vec<_>> = HashMap::new();
for (key, val) in list {
    if let Some(mut vals) = map.get_mut(&key) {
        vals.push(val);
        continue;
    }
    map.insert(key, vec![val]);
}

Ugh. Isn’t there a better way? Yes! Say hello to the Entry API.

let mut map = HashMap::new();
for (key, val) in list {
    map.entry(key).or_insert_with(Vec::new).push(val);
}

Better? I’d say so.

Omitting types

If you’ve ever used .collect(), you’ve probably come across this error:

error[E0282]: unable to infer enough type information about `_`
 --> collect.rs:2:37
  |
2 |     let numbers = (0..10).collect();
  |         ^^^^^^^ cannot infer type for `_`
  |
  = note: type annotations or generic parameter binding required

Well, of course I wanted a Vec Rust?! What else could I possibly have wanted to collect into? You might know that you can tell Rust by doing:

let numbers = (0..10).collect::<Vec<isize>>()

But did you also know that you can use our friend _ to have Rust complete the type for you?

let numbers = (0..10).collect::<Vec<_>>()

You can even make the code read better by doing:

let numbers: Vec<_> = (0..10).collect();

Cloning iterators

So, you have an iterator, and you want to clone all the things in the iterator so that you can operate on owned rather than borrowed values. Maybe you’ve written code like

for element in elements.iter() {
    let element = element.clone();
    // do something with element
}

or

elements.iter().map(|e| e.clone()) // .who_knows

There’s a better way! Among all the cool things that iterator gives you, there is .cloned, which does exactly this for you. Use

for element in elements.iter().cloned() {
// or 
elements.iter().cloned()

Give me the index, please

You iterate a lot in Rust. And every now and again, it is useful to know where in the iterator you are. There are many ways to do this. If you have a list or vector, you may be tempted to emulate a good-ol’ “counter style” for loop:

for i in (0..list.len()) {
    let e = &list[i];
    // do something with i and e
}

Or if you can’t index into list directly, you might reach for the good old counter variable trick:

let mut i = 0;
for e in list.iter() {
    // do something with i and e
    i += 1;
}

These both do what you expect, but they aren’t particularly “Rust-y”. Say hello to .enumerate()

for (i, e) in list.iter().enumerate() {
    // do something with i and e
}

Partial matching

A very powerful feature that Rust shares with many other functional programming languages is pattern matching. This is found in many places in the language, but its primary application can be found in match. It turns out that match has a few tricks up its sleeves that may not be obvious to newcomers to the language.

Let’s say you have some function, foo, which returns an enum of some sort that you wish to modify before returning it. You may be tempted to write code like this

let mut v = foo();
match v {
    Enum::Bar(ref mut x) => {
        x += 1;
    },
    Enum::Baz(ref mut y) => {
        y -= 1;
    },
    _ => (), // needed to make pattern exhaustive
};
v

But match can make this much nicer:

match foo() {
    Enum::Bar(x) => Enum::Bar(x + 1),
    Enum::Baz(x) => Enum::Baz(x - 1),
    e => e,
}

Notice that the last pattern is a catch-all, which simply returns anything not captured by the previous patterns as-is. We’ve also gotten rid of the v variable entirely. Pretty neat! This example isn’t exactly the same as the one above (we are potentially allocating a new Enum:: in the first two arms), but in most cases the two are interchangeable.

match can also do things like convert all non-Bars into Bars by binding a variable to an entire pattern:

match foo() {
    f@Enum::Bar(_) => f,
    _ => Enum::Bar(0),
}

You can find a number of other cool match tricks in The Book.

Avoid warnings for unused variables

You’re implementing a method for a trait, but you don’t need all the variables:

trait Foo { fn hello(x: &str, y: bool) }

struct Bar
impl Foo for Bar {
    fn hello(x: &str, y: bool) {
        println!("{}", x);
    }
    // WARNING: unused variable `y`
}

You can get around this in a couple of ways. The idiomatic way is to replace those variables with _, which signifies do not bind this value:

impl Foo for Bar {
    fn hello(x: &str, _: bool) { // ...

You can also assign the unused variable to _ inside the function body (this is less idiomatic, but works in more places):

impl Foo for Bar {
    fn hello(x: &str, y: bool) {
        let _ = y;
        // ...

The first version above is usually what you want. However, assigning to _ actually immediately drops the value. If this is not what you want, you can instead silence the warning for individual variables by prefixing their names with a _, like so

impl Foo for Bar {
    fn hello(x: &str, _y: bool) { // ...

If you want to silence all warnings for a particular block or function, you can use a compiler directive instead (though you probably shouldn’t be doing this)

impl Foo {
    #[allow(unused_variables)]
    fn hello(x: &str, y: bool) { // ...

Enforcing documentation

Did you know that you can make the Rust compiler yell at you if you’ve forgotten to write documentation for any part of your code that is visible to outside users? Just add the following magical line to your crate entry point (probably src/lib.rs):

#![deny(missing_docs)]

Clippy and other bags of tricks

If you haven’t found it already, you should go check out Clippy, which is a linter for Rust code. It suggests changes (such as many of the above) to your code to make it more idiomatic, as well as pointing out common errors and their fixes.

If you want more of these kinds of tricks, you might also want to check out llogiq’s post Rustic Bits which has a similar flavor to this one.

Other things?

Some neat points have already been brought up in the Reddit thread for this post. I will incorporate new ones as they come. If you know of other tricks that should go in this post, feel free to post there, get in touch, or send a GitHub PR, and I’ll be happy to take a look.