5 Generics, option, and result

This chapter covers

Rust is a strict language with concrete types, but after this chapter, you’ll have three important tools to work with. Generics let you describe to Rust “some sort of type” that Rust will turn into a concrete type without you having to do it. After that, we’ll learn about two interesting enums called Option and Result. Option tells Rust what to do when there might not be a value, and Result tells Rust what to do when something might go wrong.

5.1 Generics

We’ve known since chapter 1 that Rust needs to know the concrete type for the input and output of a function. The following return_item() function has i32 for both its input and output, and no other type will work—only i32:

fn return_item(item: i32) -> i32 {
    println!("Here is your item.");
    item
}
 
fn main() {
    let item = return_item(5);
}

But what if you want the function to accept more than i32? It would be annoying if you had to write all these functions:

fn return_i32(number: i32) -> i32 {  }
fn return_i16(number: i16) -> i16 {  }
fn return_u8(number: u8) -> u8 {  }      

And so on, and so on.

You can use generics for this. Generics basically means “maybe one type, maybe another type.”

For generics, you use angle brackets with the type inside, like this: <T>. This means “any type you put into the function.” Rust programmers usually use one capital letter for generics (T, U, V, etc.), but the name doesn’t matter, and you don’t have to use one letter. The only part that matters is the angle brackets: <>.

This is how you change the function to make it generic:

fn return_item<T>(item: T) -> T {
    println!("Here is your item.");
    item
}
 
fn main() {
    let item = return_item(5);
}

The important part is the <T> after the function name. Without this, Rust will think that T is a concrete (concrete = not generic) type, like String or i8. When talking about generics, people say that something is “generic over (name of the type).” So, for the return_item function, you would say, “The function return_item is generic over type T.”

Type names in generics are easier to understand if we choose a name instead of just T. See what happens when we change T to MyType:

fn return_item(item: MyType) -> MyType {
    println!("Here is your item.");
    item
}

The compiler gives the error cannot find type `MyType` in this scope. As you can see, MyType is concrete, not generic: the compiler is looking for something called MyType and can’t find it. To tell the compiler that MyType is generic, we need to write it inside the angle brackets:

fn return_item<MyType>(item: MyType) -> MyType {
    println!("Here is your item.");
    item
}
 
fn main() {
    let item = return_item(5);
}

Because of the angle brackets, now the compiler sees that this is a generic type that we are calling MyType. Without the angle brackets, it’s not generic.

Let’s look at the first part of the signature one more time to make sure we understand it. Here is the signature:

fn return_item<MyType>(item: MyType)

The compiler reads this as

You could call it anything as long as you put it in angle brackets so the compiler knows that the type is generic. Now, we will go back to calling the type T because Rust code usually uses single letters. You can choose your own names in your own generic code, but it’s good to get used to seeing these single letters and recognizing them as a hint that we are dealing with generic types.

You will remember that some types in Rust are Copy, some are Clone, some are Display, some are Debug, and so on. In other words, they implement the traits Copy, Clone, and so on. With Debug, we can print with {:?}.

The following code sample tries to print a generic item called T, but it won’t work. Can you guess why?

fn print_item<T>(item: T) {
    println!("Here is your item: {item:?}");
}
 
fn main() {
    print_item(5);
}

The function print_item() needs T to have Debug to print item, but is T a type with Debug? Maybe not. Maybe it doesn’t have #[derive(Debug)]—who knows? The compiler doesn’t know either, so it gives an error:

error[E0277]: `T` doesn't implement `Debug`
 --> src/main.rs:2:34
  |
2 |     println!("Here is your item: {item:?}");
  |                                  ^^^^^^^^ `T` cannot be formatted 
  using `{:?}` because it doesn't implement `Debug`

There’s no guarantee that T implements Debug. Somebody using the function might pass in a type that implements Debug, but also might not! Do we implement Debug for T? No, because we don’t know what T is—right now, anyone can use the function and put in any type. Some of them will have Debug; some won’t.

However, we can tell the function: “Don’t worry, any type T that we pass into this function will implement Debug.” It’s sort of a promise to the compiler:

use std::fmt::Debug;                           
 
fn print_item<T: Debug>(item: T) {             
    println!("Here is your item: {item:?}");
}
 
fn main() {
    print_item(5);
}

The Debug trait is located at std::fmt::Debug.

<T: Debug> is the important part.

Now the compiler knows: “Okay, this type T is going to have Debug.” Now the code works because i32 has Debug. Now, we can give it many types: String, &str, and so on because they all have Debug. The code will now compile, and the compiler won’t let any type be the variable item in this function unless it has Debug (you can’t trick the compiler).

Now, we can create a struct and give it Debug with #[derive(Debug)], so we can print it, too. Our function can take i32, the struct Animal, and more:

use std::fmt::Debug;
 
#[derive(Debug)]
struct Animal {
    name: String,
    age: u8,
}
 
fn print_item<T: Debug>(item: T) {
    println!("Here is your item: {item:?}");
}
 
fn main() {
    let charlie = Animal {
        name: "Charlie".to_string(),
        age: 1,
    };
 
    let number = 55;
 
    print_item(charlie);
    print_item(number);
}

This prints

Here is your item: Animal { name: "Charlie", age: 1 }
Here is your item: 55

Sometimes, we need more than one generic type in a generic function. To do this, we have to write out each generic type name and think about how we want to use it. What traits should each type be able to use?

In the following example, we want two types. First, we want a type called T that we would like to print. Printing with {} is nicer, so we will require Display for T.

Next is a generic type that we will call U and two variables, num_1 and num_2, which will be of type U. We want to compare them, so it will need PartialOrd. The PartialOrd trait lets us use comparison operators like <, >, ==, and so on. But we want to print them, too, so we require Display for U as well. You can use + if you want to indicate more than one trait.

To sum up, <U: Display + PartialOrd> means there is a generic type that we are calling U, and it needs to have these two traits:

use std::fmt::Display;
use std::cmp::PartialOrd;
 
fn compare_and_display<T: Display, U: Display + PartialOrd>(statement: T,
input_1: U, input_2: U) {
    println!("{statement}! Is {input_1} greater than {input_2}? {}",
    input_1 > input_2);
}
 
fn main() {
    compare_and_display("Listen up!", 9, 8);
}

This prints Listen up!! Is 9 greater than 8? true. So,

fn compare_and_display<T: Display, U: Display + PartialOrd>(statement: T,
num_1: U, num_2: U) 

says the following:

We can give compare_and_display() different types if we want. The variable statement can be a String, a &str, or anything with Display.

To make generic functions easier to read, we can also use the keyword where right before the code block:

use std::cmp::PartialOrd;
use std::fmt::Display;
 
fn compare_and_display<T, U>(statement: T, num_1: U, num_2: U)   
where                                                            
    T: Display,
    U: Display + PartialOrd,
{
    println!("{statement}! Is {num_1} greater than {num_2}? {}", 
    num_1 > num_2);
}
 
fn main() {
    compare_and_display("Listen up!", 9, 8);
}

Now the part after compare_and_display only has <T, U>, which is a lot cleaner to read.

Then we use the where keyword and indicate the traits needed on the following lines.

Using where is a good idea when you have many generic types. Also note the following:

use std::fmt::Display;
 
fn say_two<T: Display, U: Display>(statement_1: T, 
statement_2: U) {                                         
    println!("I have two things to say: {statement_1} and {statement_2}");
}
 
fn main() {
    say_two("Hello there!", String::from("I hate sand."));  
    say_two(String::from("Where is Padme?"), 
    String::from("Is she all right?"));                     
}

Types T and U both need to implement Display, but they can be different types.

Type T is a &str, but type U is a String. No problem: both of these implement Display.

Here both types are String. No problem: T and U don’t have to be different types.

This prints

I have two things to say: Hello there! and I hate sand.
I have two things to say: Where is Padme? and Is she all right?

Now that we understand both enums and generics, we can understand Option and Result. These are two enums that Rust uses to help us write code that will not crash.

5.2 Option and Result

The beginning of the chapter describes Option as a type “for when you might get a value, but maybe not,” and Result as a type “for when an operation might succeed, but maybe not.” If you remember that, you should have a good idea of when to use one and when to use the other.

A person in real life, for example, would have an Option<Spouse>. You might have one, and you might not. Not having a spouse simply means that you don’t have a spouse, but it’s not an error—just something that might or might not exist.

But the function go_to_work() would return a Result because it might fail! Most times go_to_work() succeeds, but one day, it might snow too much, and you have to stay home.

Meanwhile, simple functions like print_string() or add_i32() always produce output and can’t fail, so they don’t need to deal with Option or a Result. With that in mind, let’s start with Option.

5.2.1 Option

You use Option when something might or might not exist. When a value exists, it is Some(value), and when it doesn’t, it’s None. Here is an example of bad code that can be improved with Option:

fn take_fifth_item(value: Vec<i32>) -> i32 {
    value[4]
}
 
fn main() {
    let new_vec = vec![1, 2];
    let index = take_fifth_item(new_vec);
}

This code panics when we run it. Here is the message:

thread 'main' panicked at 'index out of bounds: 
the len is 2 but the index is 4', src\main.rs:34:5

Panic means that the program stops before the problem happens. Rust sees that the function wants something impossible and stops. It “unwinds the stack” (takes the values off the stack) and tells you, “Sorry, I can’t do that.”

To fix this, we will change the return type from i32 to Option<i32>. This means “give me a Some(i32) if it’s there, and give me None if it’s not.” We say that the i32 is “wrapped” in an Option, which means it’s inside an Option. If it’s Some, you have to do something to get the value out:

fn try_take_fifth(value: Vec<i32>) -> Option<i32> {
    if value.len() < 5 {                            
        None
    } else {
        Some(value[4])
    }
}
 
fn main() {
    let small = vec![1, 2];
    let big = vec![1, 2, 3, 4, 5];
    println!("{:?}, {:?}", try_take_fifth(small), try_take_fifth(big));
}

.len() gives the length of the Vec. Here, we are checking that the length is at least 5.

This prints None, Some(5). Our program doesn’t panic anymore, so this is better than before. But in the second case, the value 5 is still inside the Option. How do we get the 5 out of there?

We can get the value inside an Option with a method called .unwrap(), but be careful with .unwrap(). It’s just like unwrapping a present: maybe there’s something good inside, or maybe there’s an angry snake inside. You only want to .unwrap() if you are sure. If you unwrap a value that is None, the program will panic:

fn try_take_fifth(value: Vec<i32>) -> Option<i32> {
    if value.len() < 5 {
        None
    } else {
        Some(value[4])
    }
}
 
fn main() {
    let small = vec![1, 2];
    let big = vec![1, 2, 3, 4, 5];
    println!("{:?}, {:?}",
        try_take_fifth(small).unwrap(),   
        try_take_fifth(big).unwrap()
    );
}

This one returns None. .unwrap() will panic!

The message is

thread 'main' panicked at 'called 
`Option::unwrap()` on a `None` value', src\main.rs:14:9

But we don’t have to use .unwrap(). We can use a match instead. With match, we can print the value if we have Some and not touch it if we have None. For example:

fn try_take_fifth(value: Vec<i32>) -> Option<i32> {
    if value.len() < 5 {
        None
    } else {
        Some(value[4])
    }
}
 
fn handle_options(my_option: &Vec<Option<i32>>) {
    for item in my_option {
        match item {
            Some(number) => println!("Found a {number}!"),
            None => println!("Found a None!"),
        }
    }
}
 
fn main() {
    let small = vec![1, 2];
    let big = vec![1, 2, 3, 4, 5];
    let mut option_vec = Vec::new();           
 
    option_vec.push(try_take_fifth(small));    
    option_vec.push(try_take_fifth(big));      
 
    handle_options(&option_vec);               
}

Makes a new Vec to hold our Options. The vec is type: Vec<Option<i32>>. That means a Vec of Option<i32>.

This pushes None into the Vec.

This pushes Some(5) into the vec.

handle_option() looks at every option in the Vec. It prints the value if it is Some. It doesn’t touch it if it is None.

This prints

Found a None!
Found a 5!

This was a good example of pattern matching. Some(number) is a pattern, and None is another pattern. We use match to decide what to do when each of these patterns happens. The Option type has two possible patterns, so we have to decide what to do when we see one pattern and what to do when we see another.

So, what does the actual Option type look like? Because we know generics, we are able to read the code for Option. It is quite simple—just an enum:

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

The important point to remember is with Some, you have a value of type T (any type). Also, note that the angle brackets after the enum name around T tell the compiler that it’s generic. It has no trait like Display or anything to limit it; it can be anything. But with None, you don’t have any value inside.

So, in a match statement for Option you can’t say

Some(value) => println!("The value is {}", value),
None(value) => println!("The value is {}", value),

because None doesn’t hold a T inside it. Only the Some variant will hold a value.

There are easier ways to use Option. In the next code sample, we will use a method called .is_some() to tell us if it is Some. (Yes, there is also a method called .is_none().) Using this means that we don’t need handle_option() anymore:

fn try_take_fifth(value: Vec<i32>) -> Option<i32> {
    if value.len() < 5 {
        None
    } else {
        Some(value[4])
    }
}
 
fn main() {
    let small = vec![1, 2];
    let big = vec![1, 2, 3, 4, 5];
    for vec in vec![small, big] {
        let inside_number = try_take_fifth(vec);
        if inside_number.is_some() {                          
            println!("We got: {}", inside_number.unwrap());   
        } else {
            println!("We got nothing.");
        }
    }
}

The .is_some() method returns true if we get Some, false if we get None.

We already checked that inside_number is Some, so it is safe to use .unwrap(). There is an easier way to do this called 'if let' that we will learn soon.

This prints

We got nothing.
We got: 5

Now imagine that we wanted this take_fifth() function or some other function to give us a reason for why it fails. We don’t want to get None; we want to know why it failed. When it fails, we’d like to have some information on what went wrong so we can do something about it. Something like Error: Vec wasn't long enough to get the fifth item. That’s what Result is for! Let’s learn that now.

5.2.2 Result

Result looks similar to Option, but here is the difference:

You often see both Option and Result at the same time. For example, you might want to get data from a server. First, you use a function to connect. The connection might fail, so that’s a Result. And after connecting, there might not be any data. That’s an Option. So the entire operation would be an Option inside a Result: a Result<Option<SomeType>>.

To compare the two, here are the signatures for Option and Result:

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

Note that Result has a value inside of Ok and inside of Err. That is because errors are supposed to contain information that describes what went wrong. Also, note that Ok holds a generic type T, and Err holds a generic type E. As we learned in this chapter, they can be different types (and usually are) but could be the same.

Result<T, E> means you need to think of what you want to return for Ok and what you want to return for Err. In fact, you can return anything you like. Even returning a () in each case is okay:

fn check_error() -> Result<(), ()> {
    Ok(())
}
 
fn main() {
    check_error();
}

check_error() says, “Return () if we get Ok, and return () if we get Err.” Then we return Ok with a () inside it. The program works with no problem!

The compiler gives us an interesting warning, though:

warning: unused `std::result::Result` that must be used
 --> src\main.rs:6:5
  |
6 |     check_error();
  |     ^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: this `Result` may be an `Err` variant, which should be handled

This is true: we only returned the Result, but it could have been an Err. So, let’s handle the error a bit, even though we’re still not really doing anything:

fn see_if_number_is_even(input: i32) -> Result<(), ()> {
    if input % 2 == 0 {
        return Ok(())
    } else {
        return Err(())
    }
}
 
fn main() {
    if see_if_number_is_even(5).is_ok() {
        println!("It's okay, guys")
    } else {
        println!("It's an error, guys")
    }
}

This prints It's an error, guys. We just handled our first error! Something went wrong, we told Rust what to do in case of an error, and the program didn’t panic. That’s what Result helps you with.

The four methods to easily check the state of an Option or a Result are as follows:

Sometimes a function with Result will use a String for the Err value. This is not a true error type yet, but it contains some information and is a little better than what we’ve done so far. Here’s a simple example showing a function that expects the number 5 and gives an error otherwise. Using a String now lets us show some extra information:

fn check_if_five(number: i32) -> Result<i32, String> {
    match number {
        5 => Ok(number),
        _ => Err(format!("Sorry, bad number. Expected: 5 Got: {number}")),
    }
}
 
fn main() {
    for number in 4..=7 {
        println!("{:?}", check_if_five(number));
    }
}

Here is the output:

Err("Sorry, bad number. Expected: 5 Got: 4")
Ok(5)
Err("Sorry, bad number. Expected: 5 Got: 6")
Err("Sorry, bad number. Expected: 5 Got: 7")

Just like unwrapping a None for Option, using .unwrap() on Err will panic:

fn main() {
    let error_value: Result<i32, &str> = 
    Err("There was an error");          
    error_value.unwrap();                 
}

A Result is just a regular enum, so we can create one whenever we like. Both Option and Result and their variants are already in scope, so we can just write Err instead of Result::Err.

Unwraps it. Boom!

The program panics and prints

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: 
"There was an error"', src/main.rs:3:17

This information helps you fix your code. src\main.rs:3:17 means “go to the folder src, then the file main.rs, and then to line 3 and column 17 where the error happened.” So you can go there to look at your code and fix the problem.

You can also create your own error types. Result functions in the standard library and other people’s code usually do this. For example, look at this function from the standard library:

pub fn from_utf8(vec: Vec<u8>) -> Result<String, FromUtf8Error>

This function takes a vector of bytes (u8) and tries to make a String. So the success case for the Result is a String, and the error case is FromUtf8Error. You can give your error type any name you want. To make a type into a true error type in Rust, it needs to implement a trait called Error. Doing so lets it be used in generic code that expects a type that implements Error in the same way that generic code might expect a type to implement Debug, Display, or PartialOrd as we saw in this chapter.

We will start to learn about traits in detail in chapter 7, but we have some more things to learn before then. One of them is more pattern matching, as Rust has a lot of other ways to do pattern matching besides the match keyword. Let’s see why we might want to use them instead of always using match.

5.2.3 Some other ways to do pattern matching

if let

Using a match with Option and Result sometimes requires a lot of code. For example, take the .get() method, which is used on a Vec to see whether there is a value at a given index. It returns an Option:

fn main() {
    let my_vec = vec![2, 3, 4];
    let get_one = my_vec.get(0);     
    let get_two = my_vec.get(10);    
    println!("{:?}", get_one);
    println!("{:?}", get_two);
}

Checks the 0th index: Some

Checks the 10th index: None

This prints

Some(2)
None

We learned that matching is a safe way to work with an Option. Let’s do that with a range from indexes 0 to 10 to see whether there are any values:

fn main() {
    let my_vec = vec![2, 3, 4];
 
    for index in 0..10 {
      match my_vec.get(index) {
        Some(number) => println!("The number is: {number}"),
        None => {}
      }
    }
}

The code works fine and prints what we expected:

The number is: 2
The number is: 3
The number is: 4

We weren’t doing anything in case of None because we were only interested in what happens when we get a Some, but we still had to tell Rust what to do in case of None. Here we can make the code shorter by using if let. Using if let means “do something if it matches, and don’t do anything if it doesn’t.” if let is for when you don’t care about matching for everything:

fn main() {
    let my_vec = vec![2, 3, 4];
 
    for index in 0..10 {
      if let Some(number) = my_vec.get(index) {
        println!("The number is: {number}");
      }
    }
}

Two important points to remember:

let else

Rust 1.65, released in November 2022, added an interesting new syntax called let else. Let’s take a look at the same if let example but add a let else and see what makes it different. First, try reading this sample on your own and think about what is different between if let and let else:

fn main() {
    let my_vec = vec![2, 3, 4];
 
    for index in 0..10 {
        if let Some(number) = my_vec.get(index) {      
            println!("The number is: {number}");
        }
        let Some(number) = my_vec.get(index) else {    
        continue;
      };
        println!("The number is: {number}");
    }
}

This is the same if let from the previous example. It only cares about the Some pattern.

This is the let else syntax. It also is only interested in the Some pattern and doesn’t care about None.

The difference between the two is as follows:

But on the next line, it prints out the variable number, so the variable has to exist at this point. So how can this work? It can work thanks to what is called diverging code. Diverging code is basically any code that lets you escape before going to the next line. The keyword continue will do this, as will the keyword break, an early return, and so on.

You can write as much as you want inside the block after else, as long as you end with diverging code. For example,

fn main() {
    let my_vec = vec![2, 3, 4];
 
    for index in 0..10 {
        let Some(number) = my_vec.get(index) else {      
            println!("Looks like we got a None!");
            println!("We can still do whatever we want inside this block");
            println!("We just have to end with 'diverging code'");
            print!("Because after this block, ");
            println!("the variable 'number' has to exist");
            println!("Time to break the loop now, bye");
            break;
       // return ();                                    
        };
        println!("The number is: {number}");
    }
}

The block after else starts here. We have a whole block to do whatever we like. We end with break. This means the code will never get to the line below, which needs the variable called number.

This is another example of diverging code. The keyword break; is used to break out of a loop, while return will return early from the function. The function main() returns an empty tuple, as we learned in chapter 3, so using return(); will return a (), the function will be over, and we never got to the line below.

You can see that we printed out quite a bit after we finally got a None. And finally, at the end of all this printing, we use the keyword break to diverge the code, and the program never got down to the next line. Here is the output:

The number is: 2
The number is: 3
The number is: 4
Looks like we got a None!
We can still do whatever we want inside this block
We just have to end with 'diverging code'
Because after this block, the variable 'number' has to exist
Time to break the loop now, bye

while let

while let is like a while loop for if let. Imagine that we have weather station data like this, in which we would like to parse certain strings into numbers:

["Berlin", "cloudy", "5", "-7", "78"]
["Athens", "sunny", "not humid", "20", "10", "50"]

To parse the numbers, we can use a method called parse::<i32>(). First is .parse(), which is the method name, followed by ::<i32>, which is the type to parse into. It will therefore try to turn the &str into an i32 and give it to us if it can. It returns a Result because it might not work (for example, if you wanted it to parse the name “Billybrobby”—that’s not a number).

We will also use .pop(). This takes the last item off of the vector:

fn main() {
    let weather_vec = vec![
        vec!["Berlin", "cloudy", "5", "-7", "78"],
        vec!["Athens", "sunny", "not humid", "20", "10", "50"],
    ];
    for mut city in weather_vec {
        println!("For the city of {}:", city[0]);              
        while let Some(information) = city.pop() {             
            if let Ok(number) = information.parse::<i32>() {   
                println!("The number is: {number}");
            }                                                  
        }
    }
}

In our data, every first item is the city name.

while let Some(information) = city.pop() means to keep going until finally city runs out of items and .pop() returns None instead of Some.

Here we try to parse the variable we called information into an i32. This returns a Result. If it’s Ok(number), we will now have a variable called number that we can print.

Nothing happens here because we only care about getting an Ok. We never see anything that returns an Err.

This will print

For the city of Berlin:
The number is: 78
The number is: -7
The number is: 5
For the city of Athens:
The number is: 50
The number is: 10
The number is: 20

This chapter was the most “rusty” one so far. That’s because the three concepts you learned, generics, Option, and Result, aren’t even in most languages! So you’re already learning concepts that many other languages don’t even have.

But you probably also noticed that they aren’t weird, abstract concepts either. They are real, practical ways to help you write and work with your code. It’s nice not to have to write a new function for every type (generics). It’s nice to check whether a value is there or not (Option). And it’s nice to check whether an error has happened and decide what to do if it does (Result). The creators of Rust took some of these ideas from exotic languages but use them in a practical manner, as you saw in this chapter.

The next chapter isn’t too hard compared to this one. In it, you’ll learn some more about Result and error handling, and we’ll see some more complex collection types than the ones you saw in chapter 3.

Summary