Generics

Pada function, Anda biasanya menuliskan apa type yang akan diambil sebagai input:

fn return_number(number: i32) -> i32 {
    println!("Here is your number.");
    number
}

fn main() {
    let number = return_number(5);
}

Tapi bagaimana apabila kita juga ingin mengambil inputnya selain dari type i32? Kita bisa menggunakan generics untuk hal ini. Generics artinya "mungkin satu type, atau mungkin juga type yang lain".

Untuk menggunakan generics, kita menggunakan angle brackets dengan menuliskan type didalamnya, seperti ini: <T>. Ini artinya "type apa pun yang Anda masukkan ke dalam function". Biasanya, generics menggunakan type dengan satu huruf besar (T, U, V, dsb.), meskipun sebenarnya Anda tidak harus hanya menggunakan satu huruf saja.

Contoh berikut menunjukkan bagaimana cara untuk mengubah sebuah function menjadi generic type:

fn return_number<T>(number: T) -> T {
    println!("Here is your number.");
    number
}

fn main() {
    let number = return_number(5);
}

Bagian terpentinya adalah <T> yang ditulis setelah nama function. Tanpa ini, Rust akan berpikir bahwa T adalah concrete type (concrete = bukan generic), seperti String atau i8.

Ini lebih mudah untuk dimengerti jika kita menuliskan sebuah nama type. Lihatlah apa yang terjadi saat kita mengubah T menjadi MyType:


#![allow(unused)]
fn main() {
fn return_number(number: MyType) -> MyType { // ⚠️
    println!("Here is your number.");
    number
}
}

Seperti yang kita lihat, MyType adalah concrete, bukan generic. Sehingga kita perlu untuk menuliskan ini untuk menjadikannya generic:

fn return_number<MyType>(number: MyType) -> MyType {
    println!("Here is your number.");
    number
}

fn main() {
    let number = return_number(5);
}

Jadi, huruf tunggal T itu adalah untuk memudahkan "mata" programmer untuk mengetahui bahwa itu adalah generic. Sedangkan bagian yang ditulis setelah nama function < > adalah untuk "mata" compiler Rust. Tanpa itu, maka ia bukanlah generic.

Sekarang kita kembali ke type T, karena code yang ditulis pada Rust biasanya menggunakan T.

Anda akan mengingat bahwa beberapa type yang ada di Rust memiliki trait Copy, beberapanya memiliki trait Clone, beberapa lagi memiliki Display, Debug, dan seterusnya. Dengan Debug, kita bisa melakukan print menggunakan {:?}. Jadi sekarang kita bisa melihat bahwa kita memiliki sebuah problem jika kita ingin mencetak output T:

fn print_number<T>(number: T) {
    println!("Here is your number: {:?}", number); // ⚠️
}

fn main() {
    print_number(5);
}

print_number memerlukan Debug untuk mencetak number. Tapi, apakah T adalah type yang memiliki trait Debug? Mungkin tidak. Mungkin ia tidak memiliki #[derive(Debug)], siapa yang tahu. Bahkan compiler pun tidak bisa mengetahuinya, sehingga hal ini akan memberikan pesan error:

error[E0277]: `T` doesn't implement `std::fmt::Debug`
  --> src\main.rs:29:43
   |
29 |     println!("Here is your number: {:?}", number);
   |                                           ^^^^^^ `T` cannot be formatted using `{:?}` because it doesn't implement `std::fmt::Debug`

T tidak mengimplementasikan trait Debug. Jadi, apakah kita akan mengimplementasikan Debug untuk T? Tidak, karena kita pun tidak tahu apa sebenarnya T tersebut. Tapi kita bisa memberitahu function tersebut: "Jangan khawatir, karena apapun type T yang berada pada function ini akan memiliki Debug".

use std::fmt::Debug; // Debug berada pada std::fmt::Debug. Jadi sekarang kita cukup menulisnya 'Debug'.

fn print_number<T: Debug>(number: T) { // <T: Debug> adalah bagian terpenting
    println!("Here is your number: {:?}", number);
}

fn main() {
    print_number(5);
}

Jadi sekarang compiler tahu: "Okay, type T ini akan memiliki Debug". Sekarang codenya berjalan, karena i32 memiliki Debug. Sekarang kita bisa memberikan inputan dari beberapa type, seperti: String, &str, dan seterusnya, karena mereka semua memiliki Debug.

Sekarang kita bisa membuat struct dan memberinya Debug dengan #[derive(Debug)], sehingga sekarang kita bisa mencetaknya juga. Function kita bisa mengambil i32, struct Animal, dan lainnya:

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);
}

Dan hasilnya adalah:

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

Terkadang kita memerlukan lebih dari satu type pada generic function. Kita perlu untuk menuliskan setiap nama typenya, dan memikirkan tentang bagaimana kita ingin menggunakannya. Pada contoh ini, kita ingin menggunakan 2 type. Pertama kita ingin untuk mencetak statement pada type T. Print menggunakan {} terlihat lebih bagus, jadinya kita memerlukan Display untuk T.

Selanjutnya adalah type U, dan 2 variablel (num_1 dan num_2) menggunakan type U (U adalah semacam angka). Kita akan membandingkan keduanya, sehingga kita menggunakan PartialOrd. Trait ini memperbolehkan kita untuk menggunakan operator seperti <, >, ==, dan yang lainnya. Dan kita ingin ingin mencetaknya juga, jadi kita juga memerlukan Display untuk type U.

use std::fmt::Display;
use std::cmp::PartialOrd;

fn compare_and_display<T: Display, U: Display + PartialOrd>(statement: T, num_1: U, num_2: U) {
    println!("{}! Is {} greater than {}? {}", statement, num_1, num_2, num_1 > num_2);
}

fn main() {
    compare_and_display("Listen up!", 9, 8);
}

Hasil cetaknya adalah Listen up!! Is 9 greater than 8? true.

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

  • Nama functionnya adalah compare_and_display,
  • Type pertama adalah T, dan ia adalah generic. Ia haruslah type yang bisa melakukan print menggunakan {}.
  • Type kedua adalah U, and ia pula adalah generic. Ia haruslah type yang bisa melakukan print menggunakan {}. Juga, ia haruslah type yang bisa melakukan perbandingan/komparasi (menggunakan >, <, dan ==).

Sekarang kita bisa memberikan compare_and_display type yang berbeda. statement bisa saja sebuah String, bisa saja &str, atau apapun yang memiliki trait Display.

Untuk membuat generic function menjadi lebih mudah untuk dibaca, kita bisa juga menuliskannya seperti dibawah ini menggunakan where tepat sebelum penulisan 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!("{}! Is {} greater than {}? {}", statement, num_1, num_2, num_1 > num_2);
}

fn main() {
    compare_and_display("Listen up!", 9, 8);
}

Menggunakan where adalah ide yang bagus apabila Anda memiliki lebih dari satu generic type.

Dan juga, perlu diingat:

  • Jika Anda memiliki variabel dengan type T dan type lainnya juga T, maka keduanya pasti sama.
  • Jika Anda memiliki variabel dengan type T dan type lainnya adalah U, maka keduanya bisa berbeda. Namun keduanya juga bisa sama.

Sebagai contoh:

use std::fmt::Display;

fn say_two<T: Display, U: Display>(statement_1: T, statement_2: U) { // Type T memerlukan Display, type U juga memerlukan Display
    println!("I have two things to say: {} and {}", statement_1, statement_2);
}

fn main() {

    say_two("Hello there!", String::from("I hate sand.")); // Type T adalah &str, namun type U adalah String.
    say_two(String::from("Where is Padme?"), String::from("Is she all right?")); // Keduanya bertype String.
}

Hasilnya adalah:

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?