Generics

Sometimes, when writing a function or data type, we may want it to work for multiple types of arguments. For example, remember our OptionalInt type?

enum OptionalInt {
    Value(i32),
    Missing,
}

If we wanted to also have an OptionalFloat64, we would need a new enum:

enum OptionalFloat64 {
    Valuef64(f64),
    Missingf64,
}

This is really unfortunate. Luckily, Rust has a feature that gives us a better way: generics. Generics are called parametric polymorphism in type theory, which means that they are types or functions that have multiple forms (poly is multiple, morph is form) over a given parameter (parametric).

Anyway, enough with type theory declarations, let's check out the generic form of OptionalInt. It is actually provided by Rust itself, and looks like this:

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

The <T> part, which you've seen a few times before, indicates that this is a generic data type. Inside the declaration of our enum, wherever we see a T, we substitute that type for the same type used in the generic. Here's an example of using Option<T>, with some extra type annotations:

let x: Option<i32> = Some(5);

In the type declaration, we say Option<i32>. Note how similar this looks to Option<T>. So, in this particular Option, T has the value of i32. On the right-hand side of the binding, we do make a Some(T), where T is 5. Since that's an i32, the two sides match, and Rust is happy. If they didn't match, we'd get an error:

let x: Option<f64> = Some(5);
// error: mismatched types: expected `core::option::Option<f64>`,
// found `core::option::Option<_>` (expected f64 but found integral variable)

That doesn't mean we can't make Option<T>s that hold an f64! They just have to match up:

let x: Option<i32> = Some(5);
let y: Option<f64> = Some(5.0f64);

This is just fine. One definition, multiple uses.

Generics don't have to only be generic over one type. Consider Rust's built-in Result<T, E> type:

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

This type is generic over two types: T and E. By the way, the capital letters can be any letter you'd like. We could define Result<T, E> as:

enum Result<A, Z> {
    Ok(A),
    Err(Z),
}

if we wanted to. Convention says that the first generic parameter should be T, for 'type,' and that we use E for 'error.' Rust doesn't care, however.

The Result<T, E> type is intended to be used to return the result of a computation, and to have the ability to return an error if it didn't work out. Here's an example:

let x: Result<f64, String> = Ok(2.3f64);
let y: Result<f64, String> = Err("There was an error.".to_string());

This particular Result will return an f64 if there's a success, and a String if there's a failure. Let's write a function that uses Result<T, E>:

fn inverse(x: f64) -> Result<f64, String> {
    if x == 0.0f64 { return Err("x cannot be zero!".to_string()); }

    Ok(1.0f64 / x)
}

We don't want to take the inverse of zero, so we check to make sure that we weren't passed zero. If we were, then we return an Err, with a message. If it's okay, we return an Ok, with the answer.

Why does this matter? Well, remember how match does exhaustive matches? Here's how this function gets used:

let x = inverse(25.0f64);

match x {
    Ok(x) => println!("The inverse of 25 is {}", x),
    Err(msg) => println!("Error: {}", msg),
}

The match enforces that we handle the Err case. In addition, because the answer is wrapped up in an Ok, we can't just use the result without doing the match:

let x = inverse(25.0f64);
println!("{}", x + 2.0f64); // error: binary operation `+` cannot be applied
           // to type `core::result::Result<f64,collections::string::String>`

This function is great, but there's one other problem: it only works for 64 bit floating point values. What if we wanted to handle 32 bit floating point as well? We'd have to write this:

fn inverse32(x: f32) -> Result<f32, String> {
    if x == 0.0f32 { return Err("x cannot be zero!".to_string()); }

    Ok(1.0f32 / x)
}

Bummer. What we need is a generic function. Luckily, we can write one! However, it won't quite work yet. Before we get into that, let's talk syntax. A generic version of inverse would look something like this:

fn inverse<T>(x: T) -> Result<T, String> {
    if x == 0.0 { return Err("x cannot be zero!".to_string()); }

    Ok(1.0 / x)
}

Just like how we had Option<T>, we use a similar syntax for inverse<T>. We can then use T inside the rest of the signature: x has type T, and half of the Result has type T. However, if we try to compile that example, we'll get an error:

error: binary operation `==` cannot be applied to type `T`

Because T can be any type, it may be a type that doesn't implement ==, and therefore, the first line would be wrong. What do we do?

To fix this example, we need to learn about another Rust feature: traits.