Sudah lama saya ingin nulis ini karena dibutuhkan untuk belajar anak-anak CES, dan akhirnya sempat juga :D.

Cow sumber gambar: https://id.wikipedia.org

Baiklah,

Yang dimaksud Cow di sini bukanlah sapi, tapi sebuah smart-pointer dengan konsep copy-on-write yang disingkat Cow.

Pertanyaanya sekarang adalah, untuk apa? Dan kapan kita perlu menggunakannya?

Beneran, kamu gak memerlukannya, asli!, kecuali kamu membuat sistem pada device yang membutuhkan performa super maksimal atau kamu seorang OCD yang percaya bahwa alokasi memori adalah dosa yang harus dihindari.

Untuk Apa?

Cow hanyalah sebuah Enum pada Rust yang memiliki 2 variasi, yakni Borrowed dan Owned, saya tidak akan menjelaskan apa itu borrowed apa itu owned saat ini, karena hanya akan menambah rumit saja, tapi mari kita coba praktekkan langsung pada kode saja:

fn truncate(text: &str, max_length:usize) -> String {
  if text.len() > max_length {
    return format!("{} ...", &text[0..max_length]);
  }
  return text.to_string();
}

Kode tersebut adalah sebuah fungsi untuk memotong text yang panjang dengan batas maksimal yang bisa ditentukan melalui parameter max_length, apabila text-nya lebih panjang dari batas yang telah ditentukan maka text-nya akan dipotong dan akan ditambahkan ... di akhir text-nya, tidak ada yang salah kan? Ya semuanya benar dan bisa di-compile dengan sempurna, namun programmer OCD akan merasa tidak nyaman dengan kode tersebut, mengapa? Coba lihat pada bagian line return text.to_string();, itu adalah sebuah kesiasiaan dan kesiasiaan adalah teman setan, ya, itu adalah kesiasiaan alokasi memory yang sebenarnya tidak diperlukan, mengapa? Karena input parameter-nya berupa referensi (text: &str) sementara return dari fungsinya adalah String, jadi ketika sebuah text input tidak lebih panjang dari max_length maka maka kembalikan text aslinya saja, dan seharusnya tidak perlu ada alokasi memory, namun karena tipe pengembaliannya adalah String maka &str tidak kompatible dengan String, harus dikonversi dulu menggunakan to_string(), dan disinilah letak masalahnya, to_string() akan melakukan alokasi memory baru untuk menampung text-nya.

Pembahasan

Terus gimana dong solusinya? Beberapa orang mungkin berpikiran: "ganti saja tipe pengembaliannya jadi referensi juga, kan jadinya gak perlu alokasi string baru" sehingga kira-kira mereka pengen nulis jadi seperti ini:

fn truncate<'a>(text:&'a str, max_length:usize) -> &'a str {
  if text.len() > max_length {
    return &format!("{} ...", &text[0..max_length]);
  }
  return text; // <-- tidak perlu ada alokasi
}

Dan ketika di-compile hasilnya:

error[E0515]: cannot return reference to temporary value
 --> src/main.rs:3:12
  |
3 |     return &format!("{} ...", &text[0..max_length]);
  |            ^---------------------------------------
  |            ||
  |            |temporary value created here
  |            returns a reference to data owned by the current function
  |
  = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)

error: aborting due to previous error

Gagal total. Karena apabila panjang input text-nya lebih dari max_length maka pengembaliannya adalah text string yang telah dimodifikasi, yakni ditambahkan karakter ... dibelakangnya, jadi di sini perlu ada alokasi memory, namun karena tipe pengembaliannya adalah berupa referensi &'a str maka kalau dikembalikan dalam String (yang owned) jadi tidak kompatible, sehingga coba ditambahlah tanda referensi didepannya: &format!(.. tapi jadi muncul error cannot return reference to tomporary value, Rust menjelaskan dengan lugas padat dan jelas, yakni kamu tidak bisa mengembalikan (mengeluarkan) referensi pada alokasi memory yang dibuat di dalam fungsi, karena di Rust alokasi memory tersebut sifatnya temporer akan dibersihkan oleh Rust begitu keluar dari scope fungsi, Rust tidak ada GC jadi semuanya akan dibersihkan sama Rust tanpa ampun, sehingga apabila kamu mengembalikan referensi pada alokasi memory dan memory-nya sudah dibersihkan maka terjadilah dangling pointer yang biasa terjadi di C dan C++, dan bisa berakibat bencana karena kamu baru saja menciptakan potentialy exploitable bug, tinggal nunggu waktu sampai 0day exploit-nya muncul di internet. Berterimakasihlah pada Rust yang menjaga kamu dari melakukan kesalahan seperti ini tanpa mengorbankan performa.

Nah lalu solusinya gimana?

Solusi

Karena fungsi truncate itu kemungkinan pengembaliannya ada dua: dimodif textnya atau biarkan begitu adanya maka kita bisa mengembalikan dalam tipe enum saja yang bisa memiliki variasi berbeda-beda, syukurnya Rust telah menyediakannya tanpa perlu kita membuatnya, enum itu bernama: Cow, nah Cow ini kan punya dua varian: Borrowed dan Owned, untuk pengembalian yang perlu alokasi memory seperti apabila text-nya melebihi batas yang ditentukan maka kita potong dan tambahi ... dibelakangnya kita bisa menggunakan Owned untuk menampungnya, sehingga ketika dikeluarkan dari fungsi maka pemanggil fungsi akan menjadi penanggung jawab baru untuk membersihkan memorinya nanti, semetara apabila text-nya tidak lebih panjang dari yang ditentukan maka gunakan Borrowed sebagai tipe data pengembaliannya yang mana Borrowed ini hanya pinjaman dari 'a yakni dari pemanggil fungsi di atasnya sehingga tidak perlu ada alokasi memory baru karena toh penanggung jawabnya masih fungsi di atasnya bukan fungsi truncate, sehingga code terakhir yang optimal kita bisa tulis seperti ini:

fn truncate<'a>(text: &'a str, max_length: usize) -> Cow<'a, str> {
  if text.len() > max_length {
    return Owned(format!("{} ...", &text[0..max_length]));
  }
  return Borrowed(text);
}

Nah dengan begini kode truncate ini sudah optimal.

Happy Coding!

[] Robin Sy

UP 🙏🙏🙏