Option and Result

Why Option and Result?

Many languages use null\ nil\ undefined types to represent empty outputs, and Exceptions to handle errors. Rust skips using both, especially to prevent issues like null pointer exceptions, sensitive data leakages through exceptions, etc. Instead, Rust provides two special generic enums;Option and Result to deal with above cases.

💭 In the previous sections, we have discussed about the basics of enums, generics and Result & Option types.

As you know,

  • An optional value can have either Some value or no value/ None.
  • A result can represent either success/ Ok or failure/ Err
// An output can have either Some value or no value/ None.
enum Option<T> { // T is a generic and it can contain any type of value.
    Some(T),
    None,
}

// A result can represent either success/ Ok or failure/ Err.
enum Result<T, E> { // T and E are generics. T can contain any type of value, E can be any error.
    Ok(T),
    Err(E),
}

💭 Also as we discussed in preludes, not only Option and Result, and also their variants are in preludes. So, we can use them directly without using namespaces in the code.

Basic usages of Option

When writing a function or data type,

  • if an argument of the function is optional,
  • if the function is non-void and if the output it returns can be empty,
  • if the value of a property of the data type can be empty,

we have to use their data type as an Option type.

For example, if the function outputs a &str value and the output can be empty, the return type of the function should be set as Option<&str>.

fn get_an_optional_value() -> Option<&str> {

    //if the optional value is not empty
    return Some("Some value");
    
    //else
    None
}

In the same way, if the value of a property of a data type can be empty or optional like the middle_name of the Name data type in the following example, we should set its data type as an Option type.

struct Name {
  first_name: String,
  middle_name: Option<String>, // middle_name can be empty
  last_name: String,
}

💭 As you know, we can use pattern matching to catch the relevant return type (Some/ None) via match. There is a function in std::env called home_dir() to get the current user’s home directory. However, not all users have a home directory in systems like Linux, so the home directory of a user can be optional. So it returns an Option type; Option<PathBuf>.

use std::env;

fn main() {
    let home_path = env::home_dir();
    match home_path {
        Some(p) => println!("{:?}", p), // This prints "/root", if you run this in Rust playground
        None => println!("Can not find the home directory!"),
    }
}

⭐ However, when using optional arguments with functions, we have to pass None values for empty arguments while calling the function.

fn get_full_name(fname: &str, lname: &str, mname: Option<&str>) -> String { // middle name can be empty
  match mname {
    Some(n) => format!("{} {} {}", fname, n, lname),
    None => format!("{} {}", fname, lname),
  }
}

fn main() {
  println!("{}", get_full_name("Galileo", "Galilei", None));
  println!("{}", get_full_name("Leonardo", "Vinci", Some("Da")));
}

// 💡 Better create a struct as Person with fname, lname, mname fields and create a impl function as full_name()

🔎 Other than that, Option types are used with nullable pointers in Rust. Because there are no null pointers in Rust, the pointer types should point to a valid location. So if a pointer can be nullable, we have use Option<Box<T>> .

Basic usages of Result

If a function can produce an error, we have to use a Result type by combining the data type of the valid output and the data type of the error. For example, if the data type of the valid output is u64 and error type is String, the return type should be Result<u64, String>.

fn function_with_error() -> Result<u64, String> {
  
    //if error happens
    return Err("The error message".to_string());

    // else, return valid output
    Ok(255)
}

💭 As you know, we can use the pattern matching to catch the relevant return types (Ok/Err) via match. There is a function to fetch the value of any environment variable in std::env called var(). Its input is the environment variable name. This can produce an error if we pass a wrong environment variable or the program cannot extract the value of the environment variable while running. So, its return type is a Result type; Result<String, VarError>.

use std::env;

fn main() {
    let key = "HOME";
    match env::var(key) {
        Ok(v) => println!("{}", v), // This prints "/root", if you run this in Rust playground
        Err(e) => println!("{}", e), // This prints "environment variable not found", if you give a nonexistent environment variable
    }
}

is_some(), is_none(), is_ok(), is_err()

Other than match expressions, Rust provides is_some() , is_none() and is_ok() , is_err() functions to identify the return type.

fn main() {
    let x: Option<&str> = Some("Hello, world!");
    assert_eq!(x.is_some(), true);
    assert_eq!(x.is_none(), false);

    let y: Result<i8, &str> = Ok(10);
    assert_eq!(y.is_ok(), true);
    assert_eq!(y.is_err(), false);
}

ok(), err() for Result types

In addition to that, Rust provides ok() and err() for Result types. They convert the Ok<T> and Err<E> values of a Result type to Option types.

fn main() {
    let o: Result<i8, &str> = Ok(8);
    let e: Result<i8, &str> = Err("message");
    
    assert_eq!(o.ok(), Some(8)); // Ok(v) ok = Some(v)
    assert_eq!(e.ok(), None);    // Err(v) ok = None
    
    assert_eq!(o.err(), None);            // Ok(v) err = None
    assert_eq!(e.err(), Some("message")); // Err(v) err = Some(v)
}