Generics

📖 Sometimes, when writing a function or data type, we may want it to work for multiple types of arguments. In Rust, we can do this with generics.

💭 The concept is, instead of declaring a specific data type we use an uppercase letter(or CamelCase identifier). ex, instead of x : u8 we use x : T . but we have to inform to the compiler that T is a generic type(can be any type) by adding <T> at first.

Generalizing functions

fn takes_anything<T>(x: T) { // x has type T, T is a generic type
}

fn takes_two_of_the_same_things<T>(x: T, y: T) { // Both x and y has the same type
}

fn takes_two_things<T, U>(x: T, y: U) { // Multiple types
}

Generalizing structs

struct Point<T> {
  x: T,
  y: T,
}

fn main() {
  let point_a = Point { x: 0, y: 0 }; // T is a int type
  let point_b = Point { x: 0.0, y: 0.0 }; // T is a float type
}

// 🔎 When adding an implementation for a generic struct, the type parameters should be declared after the impl as well
//   impl<T> Point<T> {

Generalizing enums

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

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

⭐️ Above Option and Result types are kind of special generic types which are already defined in Rust’s standard library.

  • An optional value can have either Some value or no value/ None.

  • A result can represent either success/ Ok or failure/ Err

    Usages of Option

// 01 - - - - - - - - - - - - - - - - - - - - - -
fn get_id_by_username(username: &str) -> Option<usize> {
    // if username can be found in the system, set userId
        return Some(userId);
    // else
        None
}

// 💭 So, on the above function, instead of setting return type as usize
//   set return type as Option<usize>
// Instead of return userId, return Some(userId)
//   else None (💡remember? last return statement no need return keyword and ending ;)

// 02 - - - - - - - - - - - - - - - - - - - - - -
struct Task {
    title: String,
    assignee: Option<Person>,
}

// 💭 Instead of assignee: Person, we use Option<Person>
// because the task has not been assigned to a specific person

// - - - - - - - - - - - - - - - - - - - - - - -
// When using Option types as return types on functions
// we can use pattern matching to catch the relevant return type(Some/None) when calling them

fn main() {
    let username = "anonymous";
    match get_id_by_username(username) {
        None => println!("User not found"),
        Some(i) => println!("User Id: {}", i)
    }
}

Usages of Result

📖 The Option type is a way to use Rust’s type system to express the possibility of absence. Result expresses the possibility of error.

// - - - - - - - - - - - - - - - - - - - - - -
fn get_word_count_from_file(file_name: &str) -> Result<u32, &str> {
  // if the file is not found on the system, return error
    return Err("File can not be found!")
  // else, count and return the word count
    // let mut word_count: u32; ....
    Ok(word_count)
}

// 💭 On the above function,
// instead panic(break) the app, when the file can not be found; return Err(something)
// or when it could get the relevant data; return Ok(data)


// - - - - - - - - - - - - - - - - - - - - - - -
// We can use pattern matching to catch the relevant return type(Ok/Err) when calling it

fn main() {
    let mut file_name = "file_a";
    match get_word_count_from_file(file_name) {
        Ok(i) => println!("Word Count: {}", i),
        Err(e) => println!("Error: {}", e)
    }
}

🔎 Many useful methods have been implemented around Option and Result types. More information can be found on std::option::Option and std::result::Result pages on Rust doc.

⭐️ Also more practical examples of options & results can be found on Error Handling section in Rust doc.