Back to the basics - optional values

We'll continue with the code from the previous chapter. If you've skipped ahead, you may want to circle back and ensure you cover that chapter, before continuing.

In our current code, we're returning the default value 0 when the user makes an input error. In essence, we are using the perfectly valid, although unlikely, age of 0 to indicate an error. Rather than relying on default values, Rust allows us to return an optional value from read_number(). This means we can return a valid u8 number, or nothing. This is achieved with the Option type.

The signature of Option is: Option< [embedded type] > Where [embedded type] is the data type we want our Option to wrap. In our case we would use Option<u8> to wrap the u8 in an Option.

Let's amend our code to reflect this.

use std::str::FromStr;

fn main() {
    println!("How old are you?");
    let optional_age = read_number();

    if optional_age.is_some() {
        println!("You are {} years old.", optional_age.unwrap());
    } else {
        println!("You've entered an invalid age");
    }
}

fn read_string() -> String {
    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("can not read user input");
    input.trim().to_string()
}

fn read_number() -> Option<u8> {
    let input = read_string();
    u8::from_str(&input).ok()
}

The return value of read_number() has been changed to return a Option<u8>. The unwrap_or(0) has been replaced with ok().

Valid return values from a Option<u8> are: Some(u8) or None. The ok() method will attempt to take a valid u8 from the from_str() method. If one is found, it will return it as Some(number) otherwise it will return None.

Because the u8 is now wrapped inside the Option we can test if there was a return value. Our if statement now checks for age.is_some(). Which is true when there is a valid u8 returned in the Option.

Exercise

Run the example and see what happens when you enter a 0 as input.

Much nicer, isn't it?!

There is however one nasty piece of code in our program: optional_age.unwrap().

Because we are first testing that there is a valid value in the Option, the unwrap will always succeed. So from that perspective it is a completely valid piece of code.

However, in the previous chapter I've told you to be scarce in the use of unwrap, so there must surely be a way to handle the optional return without resorting to unwrap or expect.

In Rust, we can use the if let statement for this:

use std::str::FromStr;

fn main() {
    println!("How old are you?");
    let optional_age = read_number();

    if let Some(age) = optional_age {
        println!("You are {} years old.", age);
    } else {
        println!("You've entered an invalid age");
    }
}

fn read_string() -> String {
    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("can not read user input");
    input.trim().to_string()
}

fn read_number() -> Option<u8> {
    let input = read_string();
    u8::from_str(&input).ok()
}

The if let Some(age) = optional_age does two things in one step:

  1. It attempts to assign the value inside optional_age, which is an Option<u8>, to age.
  2. It checks if this was successful.

For the most part it behaves in the same way as a regular if statement. But in the positive case, there is now a new local variable age that you can use within that block of code. age is of type u8.

There you have it! A piece of code that handles input errors elegantly, without the need for unwrap or expect.

Exercise

Rewrite the above example, without the intermediate optional_age variable.

Matching results

Alternative to the if let statement, Rust offers a match statement. match allows you to run a piece of code for all possible return values. In the case of an Option, this is either Some(value) or None. We'll rewrite the above code using a match block.

use std::str::FromStr;

fn main() {
    println!("How old are you?");
    let optional_age = read_number();

    match optional_age {
        None => {
            println!("You've entered an invalid age");
        }
        Some(age) => {
            println!("You are {} years old.", age);
        }
    }
}

fn read_string() -> String {
    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("can not read user input");
    input.trim().to_string()
}

fn read_number() -> Option<u8> {
    let input = read_string();
    u8::from_str(&input).ok()
}

This code behaves identical to the previous example. The match statements checks the result of optional_age and runs either the None block, or the Some(age) block. The => is used as a separator, between the return value and the block that is run.

It is up to the developer - you - to decide if you prefer the if let construct, or a match block.

If you are executing only a single command in a match block, you can ditch the { pairs, and write the code more concisely. Here's the same code written in fewer lines:

use std::str::FromStr;

fn main() {
    println!("How old are you?");
    match read_number() {
        None => println!("You've entered an invalid age"),
        Some(age) => println!("You are {} years old.", age),
    }
}

fn read_string() -> String {
    let mut input = String::new();
    std::io::stdin()
        .read_line(&mut input)
        .expect("can not read user input");
    input.trim().to_string()
}

fn read_number() -> Option<u8> {
    let input = read_string();
    u8::from_str(&input).ok()
}