| Rust - Option and Result
A quick introduction to Option and Result in Rust which is a companion to the websocket game tutorial.
Apr

Introduction

This is a companion, or reference article for the series on creating a game in Rust which starts here. Its intent is to provide a baseline understanding of the Option and Result types. This is not a complete overview by any measure, and readers are encouraged to look at the official docs and Rust book for more information.

Option and Result

You are going to see both Option and Result used a lot in Rust, especially within the context of matching and function return types. A general idea of what these two is:

  • Options hold some value or None.
  • Results hold a value upon success or error upon failure.

The context in which we these are used varies, but more often than not you’re see them used in the context of function return types.

Option

Let’s take a look at the example below to see how you might use Option, and then we’ll see how it’s similar in structure to Result.

 
fn divide(numerator: f64, denominator: f64) -> Option<f64> {
    if denominator == 0.0 {
        None
    } else {
        Some(numerator / denominator)
    }
}

Here we have a function where the value returned cannot always be defined. In those cases we return None. Still, functions in Rust should return only one type in order to allow the compiler to correctly infer later computations done with the result. To get around this we use an Option which is a single type that can contain either a value or None.

In our example above, the Option<64> is saying that it’s returning a single Option type, but what it contains may or may not be a 64-bit floating point value. In essence, Option is the Schrödinger’s cat of types, with the difference being the cat has disappeared instead of being poisoned.

Before we move on I’d like to point out the line from the else block:

 
Some(numerator / denominator)

This Some is due to the fact that the Option type is defined as an Enum like so:

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

Here, the T is a generic, and is replaced by whatever type we use. In our above snippet we have Option, thus T becomes an 64-bit floating point. The reason for Some in we need a name for each element in our enum. If you wanted to, you could download the Rust source code and replace Some with something else, recompile and use that. The name is not not important, just that you realize you’re dealing with an enum.

Of course, when you write a function like the above you can’t really much with an Option once its returned. There are no arithmetic operations that allow things like:

 
let res = myOption * 5.0f;

That’s why we have both unwrap() and its more verbose partner match. Unwrap is really a shorthand way of writing a match statement which will panic and halt your program.

 
fn main() {
    let answer = divide(10.0, 2.0).unwrap();.
    println!("Answer: {}", answer);
}

You can run this and it will show 5.0 as the answer, but if you modify it so that the denominator is 0.0 it will panic and spit out the error:

 
thread 'main' panicked at src/main.rs:11:36:
called `Option::unwrap()` on a `None` value

While this may sometimes be what you want, more often than not you don’t want your entire program to stop running. For example, if this were a calculator you’d want it to display Divide by zero! or something similar. To do this we can instead use match to handle the None value by ourselves.

 
fn divide(numerator: f64, denominator: f64) -> Option<f64> {
    if denominator == 0.0 {
        None
    } else {
        Some(numerator / denominator)
    }
}

fn main() {
    let answer = divide(10.0, 0.0);
    match answer {
        Some(a) => println!("The number is: {}", a),
        None => println!("Division by 0!"),
    }
}

This is much nicer and doesn’t crash our program.

There are, of course, other ways to do this sort of thing, such as using unwrap_or or other shorthand variants of unwrap, and I’d recommend looking through the documentation to find what works best for you. Personally, I find myself using match most of the time, as it seems more explicit and usually I have some custom error handling I want to do.

One more note on Option before we move on, is that you can really use it anywhere. For example the following is fine:

 
struct Book {
    title: String,
    author: Option<String>,
    publication_year: Option<u32>,
}

fn main() {
    let book1 = Book {
        title: "The Old Man and the Sea".to_string(),
        author: Some("Ernest Hemingway".to_string()),
        publication_year: Some(1952),
    };

    let book2 = Book {
        title: "The Great Gatsby".to_string(),
        author: None,
        publication_year: Some(1925),
    };

    let book3 = Book {
        title: "One Hundred Years of Solitude".to_string(),
        author: Some("Gabriel García Márquez".to_string()),
        publication_year: None,
    };
}

Here we’re using them to store values which may or may not exist in a given set of data. A more apt example would be parsing JSON which potentially has undefined field.

Result

The Result type is actually quite similar to Option in the way it works, though with the caveat that instead of giving us None it will gives an error. Also, where Options uses Some and None, Result uses Ok and Err, with its enum being defined as such:

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

Going back to our previous of example of the division, we can rework it so that it uses Result instead of Option:

 
fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
    if denominator == 0.0 {
        Err("Cannot divide by zero.".to_string())
    } else {
        Ok(numerator / denominator)
    }
}

Here, the return type Result<f64, String> has two types, with the latter being the type of your return value. This can be any type you choose.

Many existing modules implement their own error types. For example, when you open a file it will use the std::io::Error type. Let’s first see how we might open a file and then build off that:

 
use std::fs::File;

fn main() {
    let file: File = match File::create("hello.txt") {
        Ok(f) => f,
        Err(e) => panic!("Error: {}", e)
    };    
}

This seems quite verbose compared to other languages, but thankfully we can shorten this using unwrap;

 
use std::fs::File;

fn main() {
    let file = File::create("hello.txt").unwrap();
}

Running this doesn’t do anything aside from creating our file, but it does show us that create returns a Result type with either a file of type File or an error. Let’s now write to the file and then create a function which will give us the content:

 
use std::fs::File;
use std::io::prelude::*; // To get the read_to_string trait for File

fn get_file_content(path: &str) -> Result<String, std::io::Error> {
    let mut file = File::open(path).unwrap();
    let mut content =  String::new();
    file.read_to_string(&mut content).unwrap();
    Ok(content)
}

fn main() {
    let path = "hello.txt";
    let mut file = File::create(path).unwrap(); 
    file.write_all(b"Some hello").unwrap();
    drop(file); // Take file out of scope, and thus drops it
    
    let content = get_file_content("hello.txt").unwrap();
    println!("Content: {}", content);
}

Here we see we’re returning a specific type of error (std::io::Error), but in actuality we’re not even using it, as unwrap will panic instead of deferring to returning an error. This might work for us, but what if someone wants to use our function get_file_content and instead handle the error more gracefully? For that we can use match again, or a shorthand which will pass the default error from File::open or read_to_string up to the calling function, which in our case is main. This shorthand is the question mark ?, which works similar to unwrap, but doesn’t panic. We can then handle it as see fit:

 
use std::fs::File;
use std::io::prelude::*; // To get the read_to_string trait for File

fn get_file_content(path: &str) -> Result<String, std::io::Error> {
    let mut file = File::open(path)?;
    let mut content =  String::new();
    file.read_to_string(&mut content)?;
    Ok(content)
}

fn main() {
    let path = "hello.txt";
    let mut file = File::create(path).unwrap(); 
    file.write_all(b"Some hello").unwrap();
    drop(file); // Take file out of scope, and thus drops it
    
    match get_file_content("hello.txt") {
        Ok(c) =>  println!("Content: {}", c),
        Err(_) => {}
    };
}

Here we ignore the error, using _ to substitute for any value and then an empty block {}. That means if we can’t read the content of the file it will instead do nothing.

Wrap up

This is a brief introduction to both Option and Results and I encourage you to look at the offical documentation and other tutorials to get a better understanding of how they work.