Navigating Safely with `Option` in Rust

·

6 min read

Rust is SERIOUS about safety and control over memory. Option type – an essential tool in Rust's arsenal that keeps your code safe and sound. Option is a fundamental type in Rust and is widely used for error handling, optional values, and more. You would often encounter with functions in Rust that return Option type. It encourages the safe handling of potentially absent or invalid values and helps prevent dreaded null pointer errors that plague C++.

Introducing Option

Option is an enumeration (enum) type that represents either a value or the absence of a value. It can either be "Some" or "None," helping you navigate the potentially missing or invalid values. It is defined by the standard library as follows:

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

The Option enum is so useful that it’s even included in the prelude. In other words, if a value has a type that isn’t an Option, you can safely assume that the value isn’t null.

Option Variants

The option has two variants:

  1. Some(T): Represents a value of type T. <T> is a generic type parameter. It indicates that a value is present.

  2. None: Represents the absence of a value.

Some and None are used directly without the Option:: prefix as they are included in the prelude as well.

The Null-less Rust Coast

Rust is free of the "null" feature that many other languages have. Null is a value that means there is no value there. In languages with null, variables can always be in one of two states: null or not-null. The problem? If you try to treat null as not-null, you’ll get an error and since null or not-null is a common property, it's easier to fall into this.

Sailing with Option

In Rust, Option is the ship, safeguarding you from these treacherous waters. Let's embark on this adventure with a simple example: Here the fun1 function returns an Option<f64> which means it can either return Some(f64) or None.

fn fun1()->Option<f64>{
    let x = 0.0;
    if x ==0.0{
        return Some(x);
    }
    else{
        return None;
    }
}
fn main(){
    let x = fun1();
    let y = 1.0;
    let sum = x+y;
}

error[E0369]: cannot add `{float}` to `Option<{float}>`
 --> src/main.rs:4:16
  |
4 |     let sum = x+y;
  |               -^- {float}
  |               |
  |               Option<{float}>

The expression x + y tries to add an Option<f64> (x) to a f64 (y). This is not allowed because Rust's type system enforces safety, and it doesn't allow adding an Option to a regular value directly. You would need to handle the Option using match statements or other methods to extract the value from Some (if it exists) and then perform the addition.

The Option has a large number of methods that are useful in a variety of situations like this. You can use methods like unwrap or unwrap_or to handle the Option and get its value.

Methods to work with Option

The Option enum provides several methods and associated functions that allow you to work with optional values (values that can be either Some(value) or None). Here are some of the commonly used methods for Option:

  1. unwrap(): This method extracts the value from a Some variant and panics if the variant is None. It should be used with caution, as it can lead to a panic.

  2. unwrap_or(default): Extracts the value from a Some variant or returns a default value if the variant is None. It provides a safer alternative to unwrap().

  3. unwrap_or_else(fn): Similar to unwrap_or, it takes a closure that computes and returns the default value if needed. This allows for lazy evaluation of the default value.

  4. expect(msg): Similar to unwrap(), but it allows you to provide a custom error message that will be displayed if the variant is None.

  5. is_some(): Returns true if the variant is Some, indicate the presence of a value.

  6. is_none(): Returns true if the variant is None, indicating the absence of a value.

  7. map(fn): Applies a function to the inner value if it's Some and return a new Option containing the result. If it's None, it returns None.

  8. map_or(default, fn): Applies a function to the inner value if it's Some, return the result. If it's None, it returns the provided default value.

fn main(){
    let x = fun1();
    println!("{}",x.unwrap_or(10.0));
    println!("{}",x.unwrap_or_else(|| {fun2()}));
    println!("{}",x.expect(" It has None"));
    println!("{:?}",x.is_some());
}
fn fun2()->f64{
    100.0
}
fn fun1()->Option<f64>{
    let x = 0.0;
    if x ==0.0{
        return Some(x);
    }
    else{
        return None;
    }
}
----------------------------Output---------------------
0
0
0
true

Pattern Matching with Option

fn main() {
    let x = fun1();
    match x {
        Some(x) => println!("The value is: {}", value),
        None => println!("No value found"),
    }
}

In the code above, we use match to extract and handle the value inside the Option. When Some, we print the value; when None, we indicate that no value was found.

Another example of Deserialize and Serialize JSON

Let's see an example of JSON. You are working with JSON where a few attributes are optional. In the below example, let's assume style is optional, you could get JSON as input where style is absent.

 {
"current_price": 100.0
"style":"American"
}

To handle this case, you could define struct as below:

#[derive(Clone,Debug,Deserialize,Serialize)]
pub struct Contract {
    pub current_price: f64,
    pub style: Option<String>
}
pub struct EquityOption {
    pub current_price: f64,
    pub style: Option<String>,
}
let mut contents = String::new();
file.read_to_string(&mut contents).expect("Failed to read JSON file");
let data: utils::Contract = serde_json::from_str(&contents)
                           .expect("Failed to deserialize JSON")
let mut contract = EquityOption {
    current_price: data.current_price,
    style: Option::from(data.style.as_ref().unwrap_or(&default_style))

error[E0308]: mismatched types
style : Option::from(data.style.as_ref().unwrap_or(&"European".to_string()))
  |                 ^^^^^^^^^^^^^^^^^^^^^^ expected `Option<String>`, found `Option<&String>`
  |
  = note: expected enum `Option<String>`
             found enum `Option<&String>`

Option::from is used to convert a value of one type into an Option of that type.

We used Option<String> in our equity struct that is an Option that can either hold a Some(String) value. The String inside Option<String> is owned, meaning we have exclusive ownership of this String.

The above code gives an error because we get the Option<&String> type. Option<&String> is an Option that can hold a reference to a String (&String) or None. The &String inside Option<&String> is a borrowed reference, meaning it does not have ownership of the data, and it cannot be modified or moved. It points to an existing String owned elsewhere.

Either we can change the definition of our struct and use Option<&String> then we have to make sure about the borrowing and life of the referred string. Or we can clone the string as per our use case.

style : Option::from(String::from(data.style.as_ref()
        .unwrap_or(&"European".to_string())))

.unwrap_or(&"European".to_string()) is used to provide a default value in case the Option is None. If the data.style is Some(String), this part is ignored. If the data.style was None, it creates a new String with the value "European" and returns a reference to it. Note unwrap_or expect &String hence we return a reference & .

Here is youtube video discuss about the Option<&T> and &Option<T>

Conclusion

In conclusion, you typically use Option in Rust represents optional or nullable value and wants to avoid null pointer errors and ensure safer code. You most often use when:

  • You expect a function to return a value but it might fail or return nothing, you use Option to handle the result.

  • You have a variable that might be initialized with a valid value or remain uninitialized, you use Option to represent its state.

Did you find this article valuable?

Support Siddharth by becoming a sponsor. Any amount is appreciated!