Generic types for function parameters in Rust 🦀

T PartialOrd E0308 E0369 error code

Simple example of the max function

We start with a simple function that returns the bigger of two values. Basically we are re-implementing the max function as an example.

fn main() {
    let x: i8 = 25;
    let y: i8 = 42;
    println!("{}", bigger(x, y));
}

fn bigger(a: i8, b: i8) -> i8 {
    if a > b {
        a
    } else {
        b
    }
}

This works and prints 42 as expected.

Trying to use on another type

We add the following to our code:

    let x: i16 = 25;
    let y: i16 = 42;
    println!("{}", bigger(x, y));

So now we have:

fn main() {
    let x: i8 = 25;
    let y: i8 = 42;
    println!("{}", bigger(x, y));

    let x: i16 = 25;
    let y: i16 = 42;
    println!("{}", bigger(x, y));
}

fn bigger(a: i8, b: i8) -> i8 {
    if a > b {
        a
    } else {
        b
    }
}

This will give a compile time error

error[E0308]: arguments to this function are incorrect
  --> src/main.rs:8:20
   |
8  |     println!("{}", bigger(x, y));
   |                    ^^^^^^ -  - expected `i8`, found `i16`
   |                           |
   |                           expected `i8`, found `i16`

Suggesting to read further about E0308 👎

Solution 1: separate, type specific functions

One solution is to have several implementations for the bigger function, one for each type we would like to handle. Rust 🦀 does not allow us to defined the same function name with different signatures, so we need to create functions with different names.

fn main() {
    let x: i8 = 25;
    let y: i8 = 42;
    println!("{}", bigger_i8(x, y));

    let x: i16 = 25;
    let y: i16 = 42;
    println!("{}", bigger_i16(x, y));
}

fn bigger_i8(a: i8, b: i8) -> i8 {
    if a > b {
        a
    } else {
        b
    }
}

fn bigger_i16(a: i16, b: i16) -> i16 {
    if a > b {
        a
    } else {
        b
    }
}

This already looks bad, but imagine wanting to implement this to other types as well. The body of the functions is the same so we would just duplicated the code.

Solution with generics that almost works

fn main() {
    let x: i8 = 25;
    let y: i8 = 42;
    println!("{}", bigger(x, y));

    let x: i16 = 25;
    let y: i16 = 42;
    println!("{}", bigger(x, y));
}

fn bigger<T>(a: T, b: T) -> T {
    if a > b {
        a
    } else {
        b
    }
}

Here we have only a single function that uses the proverbial type T. The letter T has no special meaning, it is not a reserved word. It is just the symbol commonly used to say "Type" or "generic Type".

In the definition of the function, immediately after the name of the function we declare in angle brackets the name(s) of the type(s) we are going to use. Once we declare that we use the name T for a generic type we can use that type anywhere in the function. So we use it to say both parameters (a and b) are going to be of this type T and that the return values is also going to be the same type T.

However, in the declaration we don't say what real type is this type T. It is a generic type.

fn bigger<T>(a: T, b: T) -> T {

To emphasize the name T is just the name commonly used in Rust 🦀, you could use any name there. This would work exactly the same way:

fn bigger<Bla>(a: Bla, b: Bla) -> Bla {

and it would make a lot more sense to me 😁.

This code almost works. Which is to say it does NOT work. This is the error we get:

error[E0369]: binary operation `>` cannot be applied to type `T`
  --> src/main.rs:12:10
   |
12 |     if a > b {
   |        - ^ - T
   |        |
   |        T
   |
help: consider restricting type parameter `T`
   |
11 | fn bigger<T: std::cmp::PartialOrd>(a: T, b: T) -> T {
   |            ++++++++++++++++++++++

It also points us to E0369 👎

Solution

In this case the solution is to follow the instructions in the error message and add the PartialOrd trait to the definition of the type. So now we have this line:

fn bigger<T: std::cmp::PartialOrd>(a: T, b: T) -> T {

or using Bla as the name of the type:

fn bigger<Bla: std::cmp::PartialOrd>(a: Bla, b: Bla) -> Bla {

This means that the function will accept any type that implements the std::cmp::PartialOrd.

That makes sense. In the function we use the greater-than operator which is implement on each type in the PartialOrd trait, so our function cannot operate on any type that does not have this trait.

Basically in the angle brackets we declare an arbitrary type name T that can be replaced by any type that has the PartialOrd trait. Then we use this type T to declare the function accepting two variables of this type and returning a value of the same type.

The code now looks like this:

examples/generic-bigger/src/main.rs

fn main() {
    let x: i8 = 25;
    let y: i8 = 42;
    println!("i8:  {}", bigger(x, y));

    let x: i16 = 25;
    let y: i16 = 42;
    println!("i16: {}", bigger(x, y));

    let x = 25; // defaults to i32
    let y = 42; // defaults to i32
    println!("i32: {}", bigger(x, y));

    let x: i64 = 25;
    let y: i64 = 42;
    println!("i64: {}", bigger(x, y));

    let x: f32 = 25.0;
    let y: f32 = 42.0;
    println!("f32: {}", bigger(x, y));

    let x = 25.0; // defaults to f64
    let y = 42.0; // defaults to f64
    println!("f64: {}", bigger(x, y));

    let x = "hello";
    let y = "world";
    println!("str: {}", bigger(x, y));

    let x = 'h';
    let y = 'w';
    println!("char: {}", bigger(x, y));

}

fn bigger<T: std::cmp::PartialOrd>(a: T, b: T) -> T {
    if a > b {
        a
    } else {
        b
    }
}

Conclusion

Generics are fun and there is a lot more to learn about them.

Related Pages

Compiler Error codes in Rust
Compiler Error codes in Rust

Author

Gabor Szabo (szabgab)

Gabor Szabo, the author of the Rust Maven web site maintains several Open source projects in Rust and while he still feels he has tons of new things to learn about Rust he already offers training courses in Rust and still teaches Python, Perl, git, GitHub, GitLab, CI, and testing.

Gabor Szabo