Dev Notes

Software Development Resources by David Egan.

Set Default Values in Rust


Rust
David Egan

Rust provides many ways to set a default value - or to fallback gracefully in the event of an error. This article focuses on how to handle Option types.

The Option<T> type represents an optional value. An Option<T> can either contain Some() value, or None (which is self-explanatory). Specifically, Option<T> is an enum with two variants:

  • None: indicating lack of value or failure
  • Some(value): A tuple struct that wraps a value of type T

Unwrapping an Option

If the Option is a Some variant, the value can be accessed using the unwrap() method:

let x: Option<u8> = Some(42);
println!("x unwraps to give the value {}", x.unwrap());
// x unwraps to give the value 42

Trying to unwrap() a None variant causes a panic!, which is not a particulary elegant way of terminating early:

let y: Option<u8> = None;
println!("unwrapping y will cause a panic! {}", y.unwrap()); 
// thread 'main' panicked at 'called `Option::unwrap()` on a `None` value'...

Unwrap, Or?

If you use the unwrap_or() method to unwrap an Option<T>, you get to specify a fallback/default value.

This ensures that the programme does not panic and is useful if the only appropriate response to the None variant is a default value:

fn main() {
    // Simulate function returning Option
    let x: Option<u8> = Some(42);	// Some variant
    let y = None;			// None variant
    
    let a = x.unwrap_or(13);
    let b = y.unwrap_or(13);
    
    println!("a = {}", a);
    println!("b = {}", b);
}

// Output:
// a = 42
// b = 13

Another example that uses Option<&'static str>:

fn main() {
    // Simulate function returning Option of Some() variant:
    let message: Option<&'static str> = Some("+++MELON MELON MELON+++");

    // Simulate function returning Option of None variant:
    let empty_message = None;
    
    let a = message.unwrap_or("+++Divide By Cucumber Error. Please Reinstall Universe And Reboot +++");
    let b = empty_message.unwrap_or("+++Divide By Cucumber Error. Please Reinstall Universe And Reboot +++");
    
    println!("a = {}", a);
    println!("b = {}", b);
}

// Output:
// a = +++MELON MELON MELON+++
// b = +++Divide By Cucumber Error. Please Reinstall Universe And Reboot +++

Let Match

A match statement can be used to match against an Option<T> pattern, which allows for more complex handling of each variant.

This example accomplishes the same thing as the one above - as such it’s not that useful, but is presented as a comparison:

fn main() {

    // Match is an expression as well as a statement. This example shows how
    // a variable is set based on an Option enum.
    // ------------------------------------------
    let msg_option = Some("+++MELON MELON MELON+++");   // Some variant
    let msg_option_empty = None;                        // None variant

    let msg = match msg_option {
        Some(m) => m,   // m is the unwrapped data from the option
        None => "Out of Cheese Error",
    };
    println!("msg = {}", msg);
    
    // As above, but handling the None variant
    let msg_2 = match msg_option_empty {
        Some(m) => m,   // In this case we won't enter this branch
        None => "Out of Cheese Error", // msg_2 set to this value
    };
    println!("msg_2 = {}", msg_2);
}

// Output:
// msg = +++MELON MELON MELON+++
// msg_2 = Out of Cheese Error

The following example shows how we might handle Option return from a function.

In this case, if the function returns the None variant, we print a message and end the programme early:

fn main() {
    let i = 5;
    //let input: Option<u8> = Some(i);
    let input: Option<u8> = None;
    
    // If the Option returned by square() has Some() value, set s to this value.
    // Otherwise, return early.
    let s = match square(input) {
        Some(ans) => ans,
        None => {
            println!("square() did not return a value.");
            return
        },
    };
    // We didn't return early, so s has a value. There is no need to unwrap.
    println!("{operand} * {operand} = {}", s, operand = i);
}

/// Check if arg has a value. If it does, set val to the value.
/// Otherwise, return None. Once we have a val, return the square.
fn square(arg: Option<u8>) -> Option<u8> {
    let val = if let Some(val) = arg {
        val
    } else {
            return None;
    };
    Some(val * val)
}

The if let Idiom

In the square() function in the example above, you could write the guard clause as:

let val = if let Some(val) = arg { val } else { return None; };

// If arg Option destructures, set val to it's value. Otherwise, enter the else block.

This has the effect of setting val to the value of arg if it is the Some() variant. If arg is the None variant, the function returns early with the returned Option being the None variant.

The if let idiom combines a pattern match and an if statement.

If you just want to operate on the value held by the Option when it is of the Some() variant:

fn main() {
    let n = Some(42); // The Option
    
    // If you just want to operate on the value held by the `Option`.
    // If the option is the `None` variant, do nothing.
    // "If n is the `Some()` variant, bind it's value to r and use this in the
    // following block."
    if let Some(r) = n {
        // We only enter this block if n is the Some() variant 
        print_num(r);
    }
    
    // This has the same result as the above if let block:
    match n {
        Some(r) => print_num(r),
        _ => {},
    }
}

Failure Case/Fallback Required

fn main() {
    let n = None;//Some(42);
    let fallback = 17;
    
    // If a failure case is required.
    // "If n is the `Some()` variant, bind it's destructured value to r and pass this
    // to the following block. Otherwise, enter the else block." 
    if let Some(r) = n {
        print_num(r);
    } else {
        print_num(fallback);
    }
    
    // This has the same result as the above:
    match n {
        Some(r) => print_num(r),
        _ => print_num(fallback),
    }
} 

Save Destructured Value into a Variable with a Default

The following example shows how to save the destructured value into a variable (in the event of the Option being the Some() variant) with a default value as a fallback:

fn main() {
    let n = None;//Some(42);
   
    // Save the destructured value into a variable.
    // If the Option variant is Some(), destructure it's value and save it into
    // the variable `result`. In the case of the `None` variant, set result to
    // the default value of 13.
    let result = if let Some(r) = n { r } else { 13 };
    
    // Equivalent to the above:
    let result_2 = match n {
        Some(x) => x,
        None => 13,
    };
    
    println!("result = {}", result);
    println!("result _2 = {}", result_2);
}

References


comments powered by Disqus