Another Rust Feature I Like


Much like this previous post, I would like to do another show-don't-tell about Rust features I like. I think a lot of Rust threads devolve into religious wars, one side saying that C++ (or whatever) sucks, the other side saying Rust is woke or something. I want to explain why I enjoy it through the use of actual code (what a concept)!


Rust doesn't have null pointer exceptions. That's it; that's the post. 😂

Seriously though the reason Rust doesn't have null pointer exceptions is that Rust doesn't have null pointers (outside of unsafe and very special circumstances). If you have a reference to something, you can be certain it exists. But, "this doesn't exist" is a totally reasonable concept to have, right? So how does Rust express that?

There's a type called Option<T>, which means "something that may or may not exist." Building on enums from yesterday:

pub enum Option<T> {
    None,
    Some(T),
}

This is an enum with two variants, one of which is associated with a value and one of which isn't. If I pass someone an Option<i32> then they know that it might not be there. In order to treat it as a number they have to match it:

match some_opt {
    Some(val) => println!("The number is {}", val),
    None => println!("I see no number here")
}

Because of compiler checks, those matches have to be exhaustive; you have to cover both cases.

Wow what a pain the butt, right? So of course there are a bunch of shorthands. Here are a few:

  • First, if you can logically prove it's there (but the compiler can't), you can just .unwrap() it. That's short for something like:
match some_opt {
    Some(val) => val,
    None => panic!("oh no!")
}
  • There's also .expect which is the same thing but lets you specify the message it panics with:
let val = something.expect("no val; maybe the frobnicator didn't initialize?")
  • Option also implements the IntoIterator trait, meaning you can treat it like an iterator of either 0 or 1 things, meaning you get .map() and other usual tools.
  • There are also some helper methods like .map_or_default() and so on.
  • And then there's if let.

The if let construct is Rust recognizing that this is a very common pattern to write:

if opt.is_some() {
    let Some(val) = opt;
    something_with_val(val)
}

And so it lets you shorten it to this:

if let Some(val) = opt {
    something_with_val(val)
}

You can even destructure it further, because of destructuring binds (another Rust feature I like):

if let Some(Vector2 { x, y }) = some_coord {
    draw_thing_at(x, y, thing)
}

A friend of the Option type is the Result type, which looks like this:

pub enum Result<T, E> {
    Ok(T),
    Err(E)
}

If an Option is "a thing that might or might not be there," a Result is "a thing that either worked or it didn't." An example from the code I was writing last night:

match breadth_first_search(&map, start, goal, traversable) {
    Ok(path) => { ... }
    Err(UnreachableError) => { ... }
}

When you search for a path between two points, there may not actually be such a path, so you represent that by returning a Result. And the usual matching constructs (match and if let) work, as well as various helper methods:

let path = do_search().unwrap_or(some_default)

So if you know C++11, you're probably thinking, yeah we have that too. And you do! You copied it from Rust. 🙂 If you only know old-C++ though, consider how this elucidates API design: if something returns Result, I know it could fail and exactly how. If something returns (or accepts) Option, I know it might make sense for the value not to be there, and need to think about that. I can design my data model to contain Options as necessary, and enums, and I find that doing this makes me think clearly about what exactly I'm modeling with this data. When I do the same thing in something like Ruby or pre-11 C++ I still want to think like that but it doesn't give me the tools to express it; I can't say in Ruby "this is either a Vec<Coord> or a PathfindingError."