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:
.unwrap()
it. That's short for something like:match some_opt {
Some(val) => val,
None => panic!("oh no!")
}
.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..map_or_default()
and so on.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 Option
s as necessary, and enum
s, 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
."