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. HashMap
s 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-Bar
s into Bar
s 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.