rust programming

Rust Generics and Traits

Generics are an indispensable mechanism in programming languages.

While C++ implements generics through "templates," C language lacks a generic mechanism, which makes it difficult to build complex type systems in C projects.

The generic mechanism is a way for programming languages to express type abstraction, typically used in classes where functionality is determined but data types are to be determined, such as linked lists and maps.

Defining Generics in Functions

Here's a method for selection sort of integer numbers:

fn max(array: &[i32]) -> i32 {
    let mut max_index = 0;
    let mut i = 1;
    while i < array.len() {
        if array[i] > array[max_index] {
            max_index = i;
        }
        i += 1;
    }
    array[max_index]
}

fn main() {
    let a = [2, 4, 6, 3, 1];
    println!("max = {}", max(&a));
}

Output:

max = 6

This is a simple program to find the maximum value, which works with i32 numeric type but cannot handle f64 type data. By using generics, we can make this function work with various types. However, not all data types can be compared, so the following code is meant to demonstrate the syntax of function generics rather than for actual execution:

fn max&lt;T&gt;(array: &[T]) -> T {
    let mut max_index = 0;
    let mut i = 1;
    while i < array.len() {
        if array[i] > array[max_index] {
            max_index = i;
        }
        i += 1;
    }
    array[max_index]
}

Generics in Structs and Enums

The Option and Result enums we learned earlier are examples of generics.

In Rust, both structs and enums can implement generic mechanisms.

struct Point&lt;T&gt; {
    x: T,
    y: T
}

This is a point coordinate struct where T represents the numeric type describing the coordinates. We can use it like this:

let p1 = Point {x: 1, y: 2};
let p2 = Point {x: 1.0, y: 2.0};

Type declaration isn't necessary here due to type inference, but type mismatches are not allowed:

let p = Point {x: 1, y: 2.0}; // This won't compile

When x is bound to 1, T is set to i32, so f64 type is not allowed. If we want to use different data types for x and y, we can use two generic identifiers:

struct Point&lt;T1, T2&gt; {
    x: T1,
    y: T2
}

In enums, generics are represented like Option and Result:

enum Option&lt;T&gt; {
    Some(T),
    None,
}

enum Result&lt;T, E&gt; {
    Ok(T),
    Err(E),
}

Both structs and enums can define methods, and these methods should also implement generic mechanisms; otherwise, generic types cannot be effectively operated upon.

struct Point&lt;T&gt; {
    x: T,
    y: T,
}

impl&lt;T&gt; Point&lt;T&gt; {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 1, y: 2 };
    println!("p.x = {}", p.x());
}

Output:

p.x = 1

Note that &lt;T&gt; must follow the impl keyword because the T that follows is based on it. We can also add methods for a specific generic type:

impl Point&lt;f64&gt; {
    fn x(&self) -> f64 {
        self.x
    }
}

The generic parameter in the impl block doesn't prevent its internal methods from having their own generic parameters:

impl&lt;T, U&gt; Point&lt;T, U&gt; {
    fn mixup&lt;V, W&gt;(self, other: Point&lt;V, W&gt;) -> Point&lt;T, W&gt; {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

The mixup method combines the x of a Point<T, U> with the y of a Point<V, W> to create a new point of type Point<T, W>.

Traits

Traits in Rust are similar to interfaces in Java, though they're not exactly the same. The similarity lies in that both are behavioral specifications that can identify which classes have which methods.

In Rust, traits are declared using the trait keyword:

trait Descriptive {
    fn describe(&self) -> String;
}

Descriptive specifies that implementers must have a describe(&self) -> String method.

Here's how we implement it for a struct:

struct Person {
    name: String,
    age: u8
}

impl Descriptive for Person {
    fn describe(&self) -> String {
        format!("{} {}", self.name, self.age)
    }
}

The format is:

impl &lt;TraitName&gt; for &lt;TypeName&gt;

In Rust, a single type can implement multiple traits, but each impl block can only implement one trait.

Default Traits

This is where traits differ from interfaces: while interfaces can only specify methods without defining them, traits can define default method implementations. Being "default" means that objects can either redefine these methods or use the default implementation:

trait Descriptive {
    fn describe(&self) -> String {
        String::from("[Object]")
    }
}

struct Person {
    name: String,
    age: u8
}

impl Descriptive for Person {
    fn describe(&self) -> String {
        format!("{} {}", self.name, self.age)
    }
}

fn main() {
    let cali = Person {
        name: String::from("Cali"),
        age: 24
    };
    println!("{}", cali.describe());
}

Output:

Cali 24

If we remove the content from the impl Descriptive for Person block, the output would be:

[Object]

Traits as Parameters

Often we need to pass functions as parameters, such as callback functions or button event handlers. While in Java this is done through instances of classes implementing interfaces, in Rust we can achieve this by passing trait parameters:

fn output(object: impl Descriptive) {
    println!("{}", object.describe());
}

Any object that implements the Descriptive trait can be passed as a parameter to this function. The function doesn't need to know about any other properties or methods of the object; it only needs to know that it implements the methods specified by the Descriptive trait. Of course, this function cannot use any other properties or methods.

Trait parameters can also be implemented using this equivalent syntax:

fn output&lt;T: Descriptive&gt;(object: T) {
    println!("{}", object.describe());
}

This is a generic-style syntax sugar that becomes particularly useful when multiple parameter types are traits:

fn output_two&lt;T: Descriptive&gt;(arg1: T, arg2: T) {
    println!("{}", arg1.describe());
    println!("{}", arg2.describe());
}

When using traits as type bounds, multiple traits can be combined using the + symbol, for example:

fn notify(item: impl Summary + Display)
fn notify&lt;T: Summary + Display&gt;(item: T)

Note: This is only for type bounds and doesn't imply usage in impl blocks.

Complex implementation relationships can be simplified using the where keyword. For example:

fn some_function&lt;T: Display + Clone, U: Clone + Debug&gt;(t: T, u: U)

Can be simplified to:

fn some_function&lt;T, U&gt;(t: T, u: U) -> i32
    where T: Display + Clone,
          U: Clone + Debug

With this syntax understood, we can now properly implement the "find maximum" example from the generics chapter:

trait Comparable {
    fn compare(&self, object: &Self) -> i8;
}

fn max&lt;T: Comparable&gt;(array: &[T]) -> &T {
    let mut max_index = 0;
    let mut i = 1;
    while i < array.len() {
        if array[i].compare(&array[max_index]) > 0 {
            max_index = i;
        }
        i += 1;
    }
    &array[max_index]
}

impl Comparable for f64 {
    fn compare(&self, object: &f64) -> i8 {
        if &self > &object { 1 }
        else if &self == &object { 0 }
        else { -1 }
    }
}

fn main() {
    let arr = [1.0, 3.0, 5.0, 4.0, 2.0];
    println!("maximum of arr is {}", max(&arr));
}

Output:

maximum of arr is 5

Tip: Since we need to declare that the second parameter of the compare function must be the same type as the type implementing the trait, the Self keyword (note the capitalization) represents the current type itself (not the instance).

Traits as Return Types

Traits can be used as return types with the following format:

fn person() -> impl Descriptive {
    Person {
        name: String::from("Cali"),
        age: 24
    }
}

However, there's one important restriction: when using a trait as a return type, all possible return values in the same function must be of exactly the same type. For example, if structs A and B both implement trait Trait, the following function would be incorrect:

fn some_function(bool bl) -> impl Descriptive {
    if bl {
        return A {};
    } else {
        return B {};
    }
}

Box<dyn Error> has runtime overhead