Program nyata jarang bekerja dengan satu tipe saja: teks masukan dari pengguna harus menjadi EmailAddress, angka hasil hitungan harus menjadi persentase f64, dan data domain harus berpindah antar lapisan aplikasi. Tanpa standar, setiap crate menyelesaikan kebutuhan ini dengan gaya sendiri — ada yang memakai new, ada parse, ada from_str, ada of — sehingga pembaca kode harus menghafal kebiasaan tiap tipe. Standard library menutup kekacauan itu dengan sepasang trait: From untuk mendefinisikan konversi yang pasti berhasil, dan Into untuk memakai konversi tersebut dari arah sebaliknya. Keistimewaan pasangan ini ada pada blanket impl di std: siapa pun yang mengimplementasikan From mendapatkan Into secara gratis, tanpa satu baris kode tambahan. Chapter ini membedah mekanisme tersebut sampai ke pesan error kompilator yang membuktikannya.

Konversi Manual yang Berulang #

Pola lama yang ingin kita ganti terlihat seperti ini: EmailAddress::new("[email protected]"), Grade::from_raw(90), OrderId::of(42) — tiga tipe, tiga nama constructor, tiga kebiasaan berbeda. Masalahnya bukan sekadar estetika. Fungsi generik tidak punya cara bertanya “apakah tipe ini bisa dikonversi menjadi tipe itu?”; setiap konversi harus ditulis eksplisit dengan nama masing-masing, dan pemanggil tidak bisa memilih arah yang paling nyaman. Trait From memperbaiki seluruhnya dengan satu kontrak bernama yang seragam: impl From<T> for U menyatakan “setiap nilai T dapat menjadi nilai U, dan konversi ini tidak mungkin gagal”.

Trait From untuk Konversi yang Pasti Berhasil #

Trait From mendefinisikan satu method from yang menerima nilai sumber dan mengembalikan nilai tujuan. Konversi yang dinyatakan lewat From harus lossless — tanpa data yang dibuang dan tanpa kemungkinan gagal. Mari kita implementasikannya untuk tipe domain EmailAddress dari Section sebelumnya:

rust
struct EmailAddress(String);

impl From<&str> for EmailAddress {
    fn from(input: &str) -> EmailAddress {
        EmailAddress(String::from(input))
    }
}

fn main() {
    let email = EmailAddress::from("[email protected]");

    println!("Email pelanggan: {}", email.0);
}

Output yang Dihasilkan:

text
Email pelanggan: [email protected]

Mari kita bedah kode di atas:

  1. Baris 3 (impl From<&str> for EmailAddress {): konversi dideklarasikan dari tipe sumber &str menuju tipe tujuan EmailAddress. Notasi impl Trait for Tipe sudah akrab dari Section Traits — bedanya, trait kali ini bawaan std.
  2. Baris 4 (fn from(input: &str) -> EmailAddress {): signature method from mengikuti kontrak trait: parameter bertipe sumber, hasil bertipe tujuan.
  3. Baris 5 (EmailAddress(String::from(input))): isi konversi bebas kita tulis; di sini teks disalin menjadi String lalu dibungkus newtype. Baris ini juga memakai From milik std — String::from — yang berperilaku sama persis dengan yang sedang kita definisikan.
  4. Baris 10 (let email = EmailAddress::from("[email protected]");): konversi dipanggil dengan path TipeTujuan::from(nilai_sumber) — nama yang sama untuk semua tipe, tidak lagi ada parse vs new vs of.

Into yang Diberikan Gratis #

Setelah From terpasang, kita mendapat hadiah tak terduga: method .into() pada nilai sumber langsung bekerja. Contoh berikut memakai dua jalur sekaligus:

rust
struct EmailAddress(String);

impl From<&str> for EmailAddress {
    fn from(input: &str) -> EmailAddress {
        EmailAddress(String::from(input))
    }
}

fn main() {
    let explicit: EmailAddress = EmailAddress::from("[email protected]");
    let implicit: EmailAddress = "[email protected]".into();

    println!("Lewat From: {}", explicit.0);
    println!("Lewat Into: {}", implicit.0);
}

Output yang Dihasilkan:

text
Lewat From: [email protected]
Lewat Into: [email protected]

Baris 11 menjadi pusat perhatian: kita tidak pernah menulis impl Into di mana pun, tetapi "[email protected]".into() berhasil menghasilkan EmailAddress. Sumber keajaibannya adalah blanket impl berikut di standard library (ditampilkan dalam bentuk sederhana):

rust
impl<T, U> Into<U> for T
where
    U: From<T>,
{
    fn into(self) -> U {
        U::from(self)
    }
}

Implementasi tersebut berbunyi: untuk semua pasangan T dan U, selama U mengimplementasikan From<T>, maka T otomatis mengimplementasikan Into<U>, dan method into-nya hanya meneruskan pekerjaan ke U::from. Karena EmailAddress mengimplementasikan From<&str>, maka &str otomatis mengimplementasikan Into<EmailAddress>. Dua hal perlu diperhatikan saat memakai .into():

  1. Anotasi tipe tujuan wajib ada. Method into tidak tahu tipe apa yang kita inginkan; baris 11 memakai let implicit: EmailAddress supaya type inference menyelesaikan U menjadi EmailAddress. Tanpa anotasi, kompilator menolak karena U tidak bisa ditebak.
  2. Arah konversi tetap satu. From<&str> for EmailAddress memberi &str → EmailAddress, bukan sebaliknya. Konversi arah balik memerlukan impl From<EmailAddress> for &str tersendiri — yang jarang masuk akal karena konversi balik biasanya bisa gagal atau membuang data.

Ketika From Tidak Tersedia #

Mekanisme blanket impl juga menjelaskan pesan error saat konversi belum didefinisikan. Bayangkan kita membuat PhoneNumber tanpa impl From, lalu memanggil .into():

rust
struct PhoneNumber(String);

fn main() {
    let phone: PhoneNumber = "0812-3456-7890".into();
    println!("Nomor: {}", phone.0);
}

Program di atas gagal dikompilasi:

text
error[E0277]: the trait bound `PhoneNumber: From<&str>` is not satisfied
 --> main.rs:4:47
  |
4 |     let phone: PhoneNumber = "0812-3456-7890".into();
  |                                               ^^^^ unsatisfied trait bound
  |
help: the trait `From<&str>` is not implemented for `PhoneNumber`
 --> main.rs:1:1
  |
1 | struct PhoneNumber(String);
  | ^^^^^^^^^^^^^^^^^^
  = note: required for `&str` to implement `Into<PhoneNumber>`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0277`.

Pesan E0277 ini layak dibaca pelan-pelan karena menuliskan rantai penalaran blanket impl secara terbuka: saran perbaikannya bukan “implementasikan Into”, melainkan “the trait From<&str> is not implemented for PhoneNumber”, dan catatannya menegaskan alasan — From<&str> pada PhoneNumber “required for &str to implement Into<PhoneNumber>”. Kompilator mengarahkan kita memperbaiki sisi From karena sisi Into akan mengikuti dengan sendirinya.

Into sebagai Bound pada Fungsi Generik #

Manfaat terbesar pasangan ini muncul pada fungsi generik. Bound T: Into<U> menyatakan “terima tipe apa saja yang bisa dikonversi menjadi U”, sehingga fungsi melayani banyak tipe sumber tanpa kita menulis kelebihan (overload) satu per satu:

rust
struct EmailAddress(String);

impl From<&str> for EmailAddress {
    fn from(input: &str) -> EmailAddress {
        EmailAddress(String::from(input))
    }
}

fn send_invoice<T>(target: T)
where
    T: Into<EmailAddress>,
{
    let address = target.into();
    println!("Invoice dikirim ke {}", address.0);
}

fn main() {
    send_invoice("[email protected]");
    send_invoice(EmailAddress(String::from("[email protected]")));
}

Output yang Dihasilkan:

text
Invoice dikirim ke [email protected]
Invoice dikirim ke [email protected]

Mari kita bedah kode di atas:

  1. Baris 11 (T: Into<EmailAddress>,): bound fungsi generik. Pemanggil boleh mengirim tipe apa pun yang mengimplementasikan Into<EmailAddress> — &str termasuk berkat impl From di baris 3.
  2. Baris 13 (let address = target.into();): di dalam fungsi, T dikonversi menjadi EmailAddress lewat blanket impl. Tidak seperti pemakaian .into() di main, anotasi tipe tidak diperlukan di sini karena tipe tujuan sudah ditentukan oleh bound.
  3. Baris 18 (send_invoice("[email protected]");): T diinferensikan sebagai &str; bound terpenuhi lewat From<&str>.
  4. Baris 19 (send_invoice(EmailAddress(String::from(...)));): T diinferensikan sebagai EmailAddress itu sendiri. Pemanggilan ini sah karena std juga menyediakan impl From<T> for T — setiap tipe dapat dikonversi menjadi dirinya sendiri (konversi refleksif), sehingga EmailAddress: Into<EmailAddress> ikut terpenuhi tanpa kode tambahan dari kita.

Konversi Bawaan di Standard Library #

Sebelum menutup, perlu dipahami bahwa std sendiri penuh dengan From — kita telah memakainya diam-diam selama ini. String::from("teks") adalah impl From<&str> for String; konversi i32 ke f64 adalah impl From<i32> for f64. Keduanya otomatis bisa juga dipanggil lewat .into():

rust
fn main() {
    let exam_score: f64 = 95i32.into();
    let customer_name: String = "Budi Santoso".into();

    println!("Nilai ujian: {}", exam_score);
    println!("Nama pelanggan: {}", customer_name);
    println!("Nama lewat String::from: {}", String::from("Budi Santoso"));
}

Output yang Dihasilkan:

text
Nilai ujian: 95
Nama pelanggan: Budi Santoso
Nama lewat String::from: Budi Santoso

Baris 2 menghasilkan f64 bernilai 95 dari i32 — konversi lossless karena setiap i32 berubah menjadi f64 tanpa kehilangan nilai pada rentang praktis. Baris 3 dan baris 7 memperlihatkan dua gaya pemanggilan yang hasilnya identik: Into versi &str ke String dan From versi String::from adalah implementasi yang sama dipanggil dari dua arah.

Ringkasan #

Trait From menstandarkan konversi lossless dengan satu method bernama from untuk semua tipe: impl From<&str> for EmailAddress membuat pemanggilan EmailAddress::from("[email protected]") tersedia, dan blanket impl di std otomatis menganugerahkan Into<EmailAddress> pada &str sehingga .into() bekerja tanpa implementasi tambahan. Bound T: Into<U> memungkinkan fungsi generik menerima semua tipe yang bisa menjadi U, termasuk U sendiri lewat From refleksif. Ketika konversi belum ada, E0277 menunjuk tepat ke From yang perlu diimplementasikan. Batasan From — konversi wajib berhasil — membuka pintu ke TryFrom dan TryInto yang mengembalikan Result untuk konversi yang bisa gagal.