Learning Rust: The Option Enum
- Published on
- Authors
- Name
- Austin Rooks
- Github
- @austionian
A Learning Opportunity
Last night I was working through another chapter of the The Rust Programming Language on closures (which themselves are really neat, but I'm not going to write about them right now). There was an example using a struct of a type bound closure and a cached value of its result. The closure was expected to contain an expensive operation that took awhile to complete. So if say we'd already given a value to that expensive operation, cache it, so we don't have to run it again.
The exercies was to use a hashmap to store to the cached values that had already been run. The initial version only and could only cache the result of a single value. It looked like this:
// ...
struct Cacher<T>
where
T: Fn(u32) -> u32,
{
calculation: T,
value: Option<u32>,
}
impl<T> Cacher<T>
where
T: Fn(u32) -> u32,
{
fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
value: None,
}
}
fn value(&mut self, arg: u32) -> u32 {
match self.value.get(&arg) {
Some(v) => v,
None => {
let v = (self.calculation)(arg);
self.value = Some(v);
v
}
}
}
}
fn generate_workout(intensity: u32, random_number: u32) {
let mut expensive_result = Cacher::new(|num| {
println!("Calculating slowly...");
thread::sleep(Duration::from_secs(2));
intensity
});
if intensity < 25 {
// If the intensity value given, hasn't already been calculated the expensive_result Cacher
// will calculate it, otherwise it will just return the cached result
println!("Today, do {} pushups!", expensive_result.value(intensity));
println!("Next, do {} situps!", expensive_result.value(intensity));
} else {
if random_number == 3 {
println!("Take a break today!");
} else {
println!(
"Today, run for {} minutes",
expensive_result.value(intensity)
);
}
}
}
So I added the
use std::collections::Hashmap;
to the top. I changed the struct defintion and implementation to something like this:
// struct
{
calculation: T,
value: Hashmap<u32, Option<u32>>,
}
// impl
{
fn new(calculation: T) -> Cacher<T> {
Cacher {
calculation,
value: Hashmap::new(),
}
}
So far so good, but as I was trying to implement the value
method that returns the cached value if it exists or does the expensive calculation and then caches that value and returns the value, I was getting mismatched type errors. My value
method look something like this:
fn value(&mut self, arg: u32) -> u32 {
match self.value.get(&arg) {
Some(v) => v,
None => {
let v = (self.calculation)(arg);
self.value.insert(arg, v);
v
}
}
}
It took me a little while of tinkering before I realized, I don't need to wrap the value in the key, value pair of the hashmap in an Option! This sounds really dumb in hindsight, but it wasn't obivous that the hashmap comes out of the box like that. The v
was already something like Some(Some(v)) and when I returned v, I was really returning Some(v), which doesn't match the expected value of u32.
:palm-to-face:
The correct looking struct defintion is just:
struct Cacher<T>
where
T: Fn(u32) -> u32,
{
calculation: T,
value: HashMap<u32, u32>, // no Option needed!
}
And the correct value
method, was just as simple:
fn value(&mut self, arg: u32) -> u32 {
match self.value.get(&arg) {
Some(v) => *v,
None => {
let v = (self.calculation)(arg);
self.value.insert(arg, v);
v
}
}
}
The interesting thing here though was that I did have to dereference the v on returning it.
Null Values in Python and JavaScript
This got me thinking, in every programming language I've ever written I've encountered a time when I forgot to check for a null value before attempting to access a null value. I spot the error and fix it, but it always feel like a workaround--there never seems to be a clean way.
Rust doesn't have a null value. The Option enum looks like this:
pub enum Option<T> {
Some(T),
None
}
It's that genius of an enum with a None that makes all the difference in giving the compiler the ability to ensure the None is always accounted for.
So I thought I'd write a post about the way other programming languages deal with this.
Python's None Object
In Python None
is used to define a null variable or object and is itself an object of the NoneType class.
None == False # False
None == '' # False
None == 0 # False
None
is not the same as False.None
is not 0.None
is not an empty string.- Comparing
None
to anything will always return False exceptNone
itself.
None is an object, but there's no Some equivilant like in Rust. It's implicitly assumed, unless explicitly None
. Which means checking needs to be done to prevent the application from panicing. (I also very much love Rust's panic!
.)
if x is not None:
# do something
With Python you need to use the idomatic if statement above using is
rather than ==
. And I really like this as it's obvious what's it's doing and why, and it's simple to read. And then it's simple to add an else:
if you want to handle the null case.
But null values values are nefarious because we exist and we assume other things exist when thinking about them. The whole fact that the world exists and is not none is mind blowing the more you think about it. That there is something and not nothing. Programming with null values in mind isn't really harmonious with how we think. With how it's fundamental that we think.
And so we find optional ways to account for nulls.
To access a dict key you can use the getattr
method. Something like:
class Car:
year = 2003
make = "Toyota"
car = Car()
make = getattr(car, make)
model = getattr(car, model) # This throws an AttributeError!
model = getattr(car, model, "Camry") # model is now Camry
For the model example you need to give the getattr an default fallback if the key doesn't exist. I don't think anyone would write it that way though, as this is probably more typical:
make = car.get(make)
model = car.gat(model)
In the above there's no AttributeError at this point, BUT there will be if you try to do anything with model
!! So you have to use the idomatic if
above whenever accessing model.
You could try using a try {} catch {}
block, so the program doesn't crash at least.
That's easy to overlook and Python will run merrily along until you try to access that model. I get it that this is a difference between compiled and interpreted languages, but clearly here Rust enforces that one makes sure every enum's case is handled before compiling.
Javascript's null
Everyone's done this and JavaScript makes it even more difficult with the fact that undefined
is a thing wholly separate from null
.
To check something that's not null simply use code like:
if (x !== null) { //...
If it strictly isn't null I'm fine to use x in some way
But oh wait, zooming out:
let x;
//...
if (x !== null) { // This is evaluated to true
X is undefined which isn't null. OK, so let's just add one more condition:
if (x !== null && x !== undefined) { //...
// OR
if (!x) { //...
Since null and undefined are truthy false values that would kind of work. But there are a lot of truthy values in JS, e.g. empty strings, NaN, 0.
There we go, but how annoying is that.
Javascript has the optional chaining operator ?
. So something that would throw an error:
let model = car.model.name;
Can instead be written as:
let model = car.model?.name; // ie if model is undefined or null don't try to access its name.