Pola pembaruan paling sering muncul di aplikasi data bukanlah “ganti nilai lama”, melainkan “tambah nilai yang mungkin belum ada”. Stok toko bertambah sepuluh unit — kalau produknya belum tercatat, mulailah dari sepuluh. Suara pemilih dihitung satu per satu — nama kandidat yang belum muncul langsung tercatat dengan satu suara. Dengan method yang sudah kita kenal, pola ini menuntut dua langkah: periksa keberadaan key, lalu pecahkan menjadi dua cabang antara mengubah dan menambah. Chapter ini memperkenalkan Entry API — pintu tunggal di HashMap yang menggabungkan pengecekan dan pembaruan dalam satu operasi, sekaligus menjadi idiom Rust yang paling sering memenangkan kontes keanggunan kode.

Masalah: Update yang Tidak Bisa Satu Langkah #

Kita tulis dulu versi manualnya agar masalahnya terlihat. Toko menerima pengiriman barang: Teh sudah ada dengan stok 20, sedangkan Kopi benar-benar baru:

rust
use std::collections::HashMap;

fn main() {
    let mut product_stock = HashMap::new();
    product_stock.insert(String::from("Teh"), 20);

    if let Some(stock) = product_stock.get_mut("Teh") {
        *stock += 10;
    } else {
        product_stock.insert(String::from("Teh"), 10);
    }

    if let Some(stock) = product_stock.get_mut("Kopi") {
        *stock += 10;
    } else {
        product_stock.insert(String::from("Kopi"), 10);
    }

    println!("Stok Teh : {:?}", product_stock.get("Teh"));
    println!("Stok Kopi: {:?}", product_stock.get("Kopi"));
}

Output yang Dihasilkan:

text
Stok Teh : Some(30)
Stok Kopi: Some(10)

Kode ini benar — Teh naik dari 20 ke 30 karena cabang Some yang berjalan, dan Kopi lahir dengan 10 karena cabang else yang berjalan. Harganya pun terbayar jelas: enam baris per produk, get_mut untuk mencari, dereference *stock untuk mengubah, dan insert terpisah untuk kasus key baru. Setiap pembaruan juga melakukan pencarian dua kali ketika key ternyata baru — sekali saat get_mut menjawab None, sekali lagi saat insert mencari posisi penyimpanan. Bayangkan pola ini berulang untuk ratusan produk setiap kali truk pengiriman datang.

Entry API: Satu Pintu untuk Dua Nasib #

Method entry menghapus seluruh percabangan manual tersebut. Versi yang sama kini terlihat seperti ini:

rust
use std::collections::HashMap;

fn main() {
    let mut product_stock = HashMap::new();
    product_stock.insert(String::from("Teh"), 20);

    let tea_stock = product_stock.entry(String::from("Teh")).or_insert(0);
    *tea_stock += 10;

    let coffee_stock = product_stock.entry(String::from("Kopi")).or_insert(0);
    *coffee_stock += 10;

    println!("Stok Teh : {:?}", product_stock.get("Teh"));
    println!("Stok Kopi: {:?}", product_stock.get("Kopi"));
}

Output yang Dihasilkan:

text
Stok Teh : Some(30)
Stok Kopi: Some(10)

Output identik dengan versi manual — dengan enam baris percabangan digantikan dua baris deklaratif. Mari kita bedah mekanismenya baris per baris:

  1. Baris 7 (product_stock.entry(String::from("Teh"))): entry menerima key dan mengembalikan nilai bertipe Entry — sebuah enum dengan dua varian: Occupied ketika key sudah berada di map, dan Vacant ketika slotnya masih kosong. Key dihitung hash-nya tepat satu kali di panggilan ini.
  2. Baris 7 (.or_insert(0)): method or_insert mengambil alih kedua varian Entry. Varian Occupied dijawab dengan mengembalikan reference mutable &mut V ke value yang sudah ada — nilai 0 diabaikan sama sekali. Varian Vacant dijawab dengan memasukkan pasangan key dan 0, lalu mengembalikan &mut V ke nilai yang baru saja dimasukkan tersebut.
  3. Baris 8 (*tea_stock += 10;): dereference menambah 10 ke ujung reference. Karena "Teh" sudah ada, or_insert(0) mengembalikan reference ke 20 yang lama — dan hasil 20 + 10 itulah Some(30) di output. Nilai default 0 tidak pernah menyentuh map.
  4. Baris 10-11 (entry(String::from("Kopi"))): skenario sebaliknya. Key "Kopi" mendapat varian Vacant, or_insert(0) menanam nilai 0 lebih dulu, barulah *coffee_stock += 10 menaikkannya menjadi 10.

Bukti terbaik perilaku or_insert justru ada di baris 7: nilai Teh tetap dilanjutkan dari 20, bukan direset ke 0 — or_insert tidak pernah menimpa value yang sudah ada. Berbeda dari insert yang selalu mengganti, or_insert hanya berperan saat slot kosong.

Dari sisi efisiensi, entry selesai dengan satu kali perhitungan hash: varian Occupied dan Vacant membawa informasi posisi internal hasil pencarian pertama, sehingga or_insert tidak mengulang pencarian. Pada versi manual, key yang baru mengalami dua kali pencarian penuh. Kepemilikan juga mengikuti aturan yang sudah kita kenal: key yang dikirim ke entry berpindah ke map — key baru disimpan, sedangkan key yang ternyata sudah ada langsung di-drop setelah perbandingan selesai.

Nilai default tentu tidak harus nol. Penghitungan stok gudang bisa memulai dari jumlah kiriman pertama dengan or_insert(50), pencatatan hadir bisa memulai dari or_insert(1), dan rekap transaksi bisa memulai dari nominal pertama dengan or_insert(amount) — bentuk polanya sama, hanya angka awalnya yang berganti. Yang tidak boleh berganti adalah polanya sendiri: periksa dan tambahkan harus terjadi dalam satu ekspresi. Pola manual contains_key diikuti insert mengundang bug halus ketika kode di antara keduanya mengubah isi map — misalnya pemanggilan fungsi lain yang ikut menyentuh product_stock. Dengan entry, jendela di antara pemeriksaan dan tindakan tidak ada sama sekali, sehingga tidak ada ruang bagi perubahan yang menyelinap.

Pola Klasik: Menghitung Suara #

Kekuatan Entry API paling terasa pada penghitungan frekuensi. Program berikut menghitung suara pemilihan nama maskot kelas dari daftar pilihan yang mentah:

rust
use std::collections::HashMap;

fn main() {
    let votes = vec!["Garuda", "Rajawali", "Garuda", "Elang", "Garuda", "Rajawali"];
    let mut vote_counts = HashMap::new();

    for team in votes {
        let count = vote_counts.entry(team).or_insert(0);
        *count += 1;
    }

    println!("Suara Garuda : {:?}", vote_counts.get("Garuda"));
    println!("Suara Rajawali: {:?}", vote_counts.get("Rajawali"));
    println!("Suara Elang  : {:?}", vote_counts.get("Elang"));
}

Output yang Dihasilkan:

text
Suara Garuda : Some(3)
Suara Rajawali: Some(2)
Suara Elang  : Some(1)

Mari kita telaah jantung programnya:

  1. Baris 4 (let votes = vec![...]): daftar suara mentah berisi enam suara dengan duplikasi — Garuda tiga kali, Rajawali dua kali, Elang sekali. Loop for team in votes memindahkan setiap &str keluar dari vector, satu suara per putaran.
  2. Baris 8 (let count = vote_counts.entry(team).or_insert(0);): suara pertama untuk sebuah nama menghasilkan varian Vacant — counter lahir dari 0. Suara berikutnya dengan nama yang sama menghasilkan varian Occupied — counter lama dipinjam secara mutable.
  3. Baris 9 (*count += 1;): keduanya berakhir di baris yang sama: tambah satu. Tidak ada if, tidak ada else, tidak ada pencarian ganda — tiga key berbeda diperlakukan dengan satu baris logika yang sama.

Coba bayangkan versi manual dari loop ini: contains_key atau get_mut di setiap putaran, cabang else untuk memasukkan counter pertama, dan seluruh ritual itu diulang enam kali. Pola entry(...).or_insert(0) yang diikuti *count += 1 adalah idiom standar Rust untuk penghitungan frekuensi — menghitung kata dalam dokumen, popularitas produk, atau jumlah kunjungan pengguna memakai bentuk yang persis sama.

Ringkasan #

Entry API menyatukan pengecekan keberadaan key dan pembaruan value menjadi satu operasi: entry(key) mengembalikan enum Entry dengan varian Occupied atau Vacant, dan or_insert(nilai_default) menutup kedua varian dengan satu reference &mut V — memakai value lama yang ada, atau menanam nilai default lebih dulu. Idiom entry(...).or_insert(0) diikuti dereference menjadi pola baku untuk menambah stok dan menghitung frekuensi. Chapter berikutnya melangkah ke penelusuran isi HashMap: cara iterasi yang jujur terhadap urutan acak, pembaruan value dalam loop, dan penghapusan entry.