Sejauh ini setiap fungsi yang kita tulis terikat pada satu tipe konkret: text_length menerima &String, subtotal menerima &OrderItem, dan seterusnya. Pendekatan itu sehat selama satu logika memang hanya melayani satu tipe data. Aplikasi nyata jarang seatur itu — logika pencari nilai terbesar dibutuhkan untuk skor latihan bertipe i32, rating performa bertipe f64, harga katalog bertipe u64, dan seterusnya. Kebutuhan yang sama pada tipe berbeda melahirkan godaan tertua di pemrograman: salin fungsi, tempel, lalu ganti tipenya. Chapter ini sengaja mengajak kita menimbang akibat salin-tempel tersebut secara jujur, karena beban inilah yang menjelaskan mengapa Rust membangun fitur generics.

Dua Fungsi yang Sama Persis #

Bayangkan kita sedang membangun fitur rekap latihan untuk tim futsal Garuda. Pelatih ingin melihat skor latihan tertinggi (bilangan bulat) sekaligus rating performa tertinggi (bilangan desimal). Dua kebutuhan, dua tipe — dan tanpa generics, dua fungsi:

rust
fn largest_score(scores: &[i32]) -> i32 {
    let mut largest = scores[0];
    for &score in scores {
        if score > largest {
            largest = score;
        }
    }
    largest
}

fn largest_rating(ratings: &[f64]) -> f64 {
    let mut largest = ratings[0];
    for &rating in ratings {
        if rating > largest {
            largest = rating;
        }
    }
    largest
}

fn main() {
    let scores = [72, 85, 90, 68];
    let ratings = [4.2, 3.8, 4.9, 4.5];

    println!("Skor latihan tertinggi: {}", largest_score(&scores));
    println!("Rating performa tertinggi: {}", largest_rating(&ratings));
}

Output yang Dihasilkan:

text
Skor latihan tertinggi: 90
Rating performa tertinggi: 4.9

Program ini berjalan dan hasilnya benar. Mari kita bedah kode di atas baris per baris:

  1. Baris 1 (fn largest_score(scores: &[i32]) -> i32 {): signature menyatakan parameter scores sebagai slice &[i32] — fungsi hanya meminjam array milik pemanggil, sesuai aturan borrowing dari Section Ownership.
  2. Baris 2 (let mut largest = scores[0];): elemen pertama menjadi kandidat awal. Karena i32 mengimplementasikan Copy, penyalinan nilai ini gratis dan tidak memindahkan kepemilikan array.
  3. Baris 3 (for &score in scores {): iterasi meminjam slice; pola &score menyalin setiap elemen &i32 menjadi i32 — pola yang sudah kita pakai pada Chapter Option di Section Struktur Data.
  4. Baris 4-5 (if score > largest { dan largest = score;): setiap elemen dibandingkan dengan kandidat; elemen yang lebih besar menggantikan kandidat lama.
  5. Baris 8 (largest): ekspresi terakhir tanpa titik koma menjadi nilai kembali fungsi.
  6. Baris 11-19 (fn largest_rating(ratings: &[f64]) -> f64 {): salin-tempel dari fungsi pertama. Body-nya identik baris demi baris; yang berbeda hanya tipe dan nama variabel — dan nama variabel pun mengikuti pola yang sama, sekadar menyesuaikan huruf pertama tipenya.
  7. Baris 25-26 (dua pemanggilan println!): dua logika yang sama dipanggil lewat dua nama fungsi berbeda — pemborosan yang akan kita timpali sebentar lagi.

Perbedaan yang Tersisa Hanyalah Tipe #

Mari kita sandingkan kedua fungsi berdampingan dan tandai setiap bedanya:

text
fn largest_score(scores: &[i32]) -> i32 {     fn largest_rating(ratings: &[f64]) -> f64 {
    let mut largest = scores[0];                  let mut largest = ratings[0];
    for &score in scores {                        for &rating in ratings {
        if score > largest {                          if rating > largest {
            largest = score;                              largest = rating;
        }                                          }
    }                                          }
    largest                                     largest
}                                              }

Tiga belas baris berhadapan dengan tiga belas baris. Seluruh perbedaan hanya tiga macam: tipe parameter (i32 menjadi f64), tipe nilai kembali (i32 menjadi f64), dan nama variabel — dan nama variabel sebenarnya perbedaan kosmetik yang bisa kita samakan. Algoritmanya: seratus persen identik. Jika minggu depan manajemen toko meminta fungsi harga tertinggi untuk katalog bertipe u64, kita akan menyalin untuk ketiga kalinya. Pada skala industri masalah ini meledak: standard library Rust harus menyediakan Vec, HashMap, dan Option untuk setiap tipe yang dipakai jutaan program — tanpa generics, std harus menulis Vec<i32>, Vec<String>, Vec<f64> sebagai tipe terpisah.

Biaya Riil dari Duplikasi #

Biaya pertama adalah perbaikan bug yang menyebar. Coba panggil largest_score(&[]) dengan slice kosong: program panics dengan pesan “index out of bounds: the len is 0 but the index is 0” — panic yang sama dengan yang pernah kita jumpai di Section Koleksi. Perbaikan yang benar (misalnya mengembalikan Option<&T> agar slice kosong ditangani tanpa panic) wajib diterapkan ke setiap salinan. Satu salinan yang terlewat berarti satu jalur data tetap rapuh, dan semakin banyak salinan semakin besar peluang terlewat.

Biaya kedua adalah pengujian dan review yang berlipat. Pengulas kode harus membandingkan salinan-salinan baris demi baris untuk memastikan tidak ada perbedaan senyap — misalnya satu salinan memakai >= sementara salinan lain memakai >, yang mengubah hasil saat dua nilai sama besar. Kompilator tidak bisa menolong di sini karena kedua salinan sama-sama sah secara tipe; perbedaan perilaku hanya terlihat oleh mata manusia yang lelah membandingkan. Kasus uji pun berlipat: keduanya harus dites terpisah padahal logikanya satu.

Biaya ketiga adalah jalan pintas yang menggoda namun keliru arah: macro. macro_rules! memang bisa memanggilkan pembuatan kedua fungsi sekaligus, tetapi macro bekerja di level teks kode — pesan errornya menjadi sulit dibaca, signature fungsi tidak lagi tampak jelas bagi pembaca, dan IDE kehilangan kemampuan menganalisisnya. Macro dirancang untuk memperluas sintaks, bukan untuk menyatukan logika yang sama pada tipe berbeda; kita akan membahas macro pada section khususnya.

Jalan Keluar: Tipe sebagai Placeholder #

Kebutuhan kita sebenarnya bisa dirumuskan dalam satu kalimat: satu definisi fungsi yang berkata “berlaku untuk tipe apa pun yang nanti ditentukan pemanggil”. Nilai sudah lama punya mekanisme semacam itu — parameter fungsi menampung nilai apa pun yang dikirim pemanggil. Yang belum ada adalah padanan untuk tipe, dan itulah yang disediakan generics. Bandingkan tiga baris berikut:

text
fn largest_score(scores: &[i32]) -> i32       fungsi untuk i32 saja
fn largest_rating(ratings: &[f64]) -> f64     fungsi untuk f64 saja
fn largest<T: PartialOrd>(list: &[T]) -> &T   fungsi untuk tipe apa pun yang bisa diperbandingkan

Baris ketiga adalah bentuk generik dari dua baris di atasnya. Bagian <T> mendeklarasikan type parameter — semacam parameter, tetapi isinya tipe, bukan nilai. Bagian T: PartialOrd adalah trait bound — syarat bahwa tipe pengganti T harus bisa diperbandingkan; detailnya menyusul pada Chapter Trait Bound. Satu definisi ini lalu dikompilasi menjadi salinan kode tipe-spesifik untuk setiap tipe yang benar-benar dipakai pemanggil — proses bernama monomorphisasi yang kita bedah tuntas di akhir section ini. Chapter berikutnya menulis fungsi generik pertama kita langkah demi langkah.

Ringkasan #

Dua fungsi pencari nilai terbesar dengan body identik menunjukkan pola duplikasi murni: algoritma sama, tipe berbeda. Salin-tempel seperti itu menuntut perbaikan bug, pengujian, dan review yang berlipat untuk setiap salinan, dan macro bukan alat yang tepat untuk menyatukannya. Jalan keluarnya adalah membuat tipe menjadi placeholder — type parameter <T> — sehingga logika ditulis sekali dan berlaku untuk banyak tipe. Chapter berikutnya memperkenalkan sintaks <T> pada fungsi beserta cara kompilator menebak tipe penggantinya.