impl Trait

Generics anonim pada posisi argumen dan nilai kembalian.

Daftar isi

Topik ini membahas impl Trait, yaitu bentuk ringkas batasan trait yang tidak memberi nama pada parameter tipenya. Pembahasan mencakup pemakaian pada posisi argumen sebagai padanan langsung fungsi generik, pemakaian pada posisi nilai kembalian untuk menyembunyikan tipe konkret, serta batasan penting bahwa satu fungsi hanya boleh mengembalikan satu tipe konkret. Setelah membaca topik ini, tanda tangan fungsi dapat dipilih antara bentuk generik bernama dan bentuk ringkas impl Trait sesuai kebutuhan, dan error dari nilai kembalian yang berbeda-beda dapat diperbaiki dengan pola yang tepat.

impl Trait pada posisi argumen#

Parameter fungsi dapat ditulis dengan tipe impl NamaTrait, yang berarti parameter tersebut menerima setiap tipe yang mengimplementasikan trait itu. Bentuk ini adalah singkatan dari fungsi generik dengan batasan, sehingga fn tampilkan(item: &impl Ringkas) setara dengan fn tampilkan_generik<T: Ringkas>(item: &T) dalam hal tipe yang diterima. Perbedaannya hanya pada penamaan, karena bentuk impl Trait tidak memberi nama pada parameter tipenya sehingga tipe itu tidak dapat disebut lagi pada tanda tangan yang sama.

rust argumen-impl.rs
trait Ringkas {
    fn ringkas(&self) -> String;
}

struct Artikel {
    judul: String,
    penulis: String,
}

struct Pengumuman {
    penulis: String,
    topik: String,
}

impl Ringkas for Artikel {
    fn ringkas(&self) -> String {
        format!("{} oleh {}", self.judul, self.penulis)
    }
}

impl Ringkas for Pengumuman {
    fn ringkas(&self) -> String {
        format!("{}: {}", self.penulis, self.topik)
    }
}

fn tampilkan(item: &impl Ringkas) {
    println!("Versi impl Trait : {}", item.ringkas());
}

fn tampilkan_generik<T: Ringkas>(item: &T) {
    println!("Versi generik    : {}", item.ringkas());
}

fn main() {
    let artikel = Artikel {
        judul: String::from("Rilis Rust Terbaru"),
        penulis: String::from("Budi"),
    };
    let pengumuman = Pengumuman {
        penulis: String::from("Citra"),
        topik: String::from("jadwal piket"),
    };

    tampilkan(&artikel);
    tampilkan(&pengumuman);
    tampilkan_generik(&artikel);
    tampilkan_generik(&pengumuman);
}
text Keluaran
Versi impl Trait : Rilis Rust Terbaru oleh Budi
Versi impl Trait : Citra: jadwal piket
Versi generik    : Rilis Rust Terbaru oleh Budi
Versi generik    : Citra: jadwal piket

Kedua fungsi menghasilkan keluaran yang identik karena keduanya menerima tipe yang sama, yaitu setiap implementor Ringkas. Pilihan antara keduanya menjadi perkara keterbacaan tanda tangan. Bentuk impl Trait lebih pendek untuk fungsi dengan satu atau dua parameter yang batasannya sederhana, sedangkan bentuk generik bernama menjadi keharusan ketika tipe yang sama perlu disebut lagi, misalnya pada dua parameter yang harus sama tipenya.

Batasan berganda tetap dapat ditulis pada posisi argumen dengan tanda +. Fragmen berikut meminta satu argumen yang sekaligus dapat diringkas dan digandakan.

rust Fragmen: impl Trait dengan beberapa batasan
// tidak-dijalankan
fn proses(item: impl Ringkas + Clone) {
    let salinan = item.clone();
    println!("{}", salinan.ringkas());
}

Fragmen tersebut tidak dijalankan karena Ringkas didefinisikan pada program lain di halaman ini dan bukan bagian dari pustaka standar. Pada tanda tangannya, impl Ringkas + Clone menyatakan kedua kemampuan sekaligus tanpa nama parameter tipe.

impl Trait pada nilai kembalian#

Posisi nilai kembalian memakai -> impl NamaTrait untuk menyatakan bahwa fungsi mengembalikan satu tipe konkret tertentu yang mengimplementasikan trait tersebut, tanpa membocorkan nama tipenya. Pemanggil hanya dapat memperlakukan hasilnya melalui metode trait itu, sedangkan kompilator tetap mengetahui tipe konkretnya dan mengompilasi pemanggilan secara statis. Pola ini menjadi pilihan utama ketika tipe hasilnya panjang ditulis atau merupakan detail internal, misalnya hasil rangkaian adapter iterator.

rust kembalian-impl.rs
trait Ringkas {
    fn ringkas(&self) -> String;
}

struct Artikel {
    judul: String,
    penulis: String,
}

struct Catatan {
    isi: String,
}

impl Ringkas for Artikel {
    fn ringkas(&self) -> String {
        format!("{} oleh {}", self.judul, self.penulis)
    }
}

impl Ringkas for Catatan {
    fn ringkas(&self) -> String {
        format!("Catatan: {}", self.isi)
    }
}

fn buat_artikel() -> impl Ringkas {
    Artikel {
        judul: String::from("Panduan Ownership"),
        penulis: String::from("Dewi"),
    }
}

fn buat_catatan() -> impl Ringkas {
    Catatan {
        isi: String::from("pinjam, pakai, kembalikan"),
    }
}

fn main() {
    let artikel = buat_artikel();
    let catatan = buat_catatan();

    println!("{}", artikel.ringkas());
    println!("{}", catatan.ringkas());
}
text Keluaran
Panduan Ownership oleh Dewi
Catatan: pinjam, pakai, kembalikan

Kedua fungsi mengembalikan impl Ringkas, tetapi masing-masing berkomitmen pada satu tipe konkret yang tetap, yaitu buat_artikel selalu menghasilkan Artikel dan buat_catatan selalu menghasilkan Catatan. Kompilator memeriksa komitmen itu, sehingga mengganti isi buat_artikel menjadi mengembalikan Catatan akan ditolak tanpa melihat pemanggilnya. Pemanggil memperoleh hasil yang dapat dipakai melalui ringkas, sementara kebebasan mengubah tipe internal tetap berada di tangan penulis fungsinya.

Satu tipe konkret per fungsi#

Batasan utama impl Trait pada nilai kembalian adalah satu fungsi hanya boleh mengembalikan satu tipe konkret. Cabang if dan else yang mengembalikan dua tipe berbeda ditolak karena kompilator tidak dapat menyatukan keduanya menjadi satu tipe hasil, meskipun keduanya mengimplementasikan trait yang sama. Contoh berikut memperlihatkan penolakan aslinya.

bash
export PATH="$HOME/.cargo/bin:$PATH"
# fungsi yang mencoba mengembalikan dua tipe berbeda lewat impl Trait
printf 'trait Ringkas {\n    fn ringkas(&self) -> String;\n}\n\nstruct Artikel {\n    judul: String,\n}\n\nstruct Catatan {\n    isi: String,\n}\n\nimpl Ringkas for Artikel {\n    fn ringkas(&self) -> String {\n        format!("artikel: {}", self.judul)\n    }\n}\n\nimpl Ringkas for Catatan {\n    fn ringkas(&self) -> String {\n        format!("catatan: {}", self.isi)\n    }\n}\n\nfn pilih(adalah_artikel: bool) -> impl Ringkas {\n    if adalah_artikel {\n        Artikel { judul: String::from("panduan") }\n    } else {\n        Catatan { isi: String::from("catatan") }\n    }\n}\n\nfn main() {\n    println!("{}", pilih(true).ringkas());\n}\n' > dua-tipe.rs
rustc --edition=2021 dua-tipe.rs 2>&1 | sed -n '1,22p'
text Keluaran
error[E0308]: `if` and `else` have incompatible types
  --> dua-tipe.rs:29:9
   |
26 | /     if adalah_artikel {
27 | |         Artikel { judul: String::from("panduan") }
   | |         ------------------------------------------ expected because of this
28 | |     } else {
29 | |         Catatan { isi: String::from("catatan") }
   | |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Artikel`, found `Catatan`
30 | |     }
   | |_____- `if` and `else` have incompatible types
   |
help: you could change the return type to be a boxed trait object
   |
25 - fn pilih(adalah_artikel: bool) -> impl Ringkas {
25 + fn pilih(adalah_artikel: bool) -> Box<dyn Ringkas> {
   |
help: if you change the return type to expect trait objects, box the returned expressions
   |
27 ~         Box::new(Artikel { judul: String::from("panduan") })
28 |     } else {
29 ~         Box::new(Catatan { isi: String::from("catatan") })

Pesan error itu sekaligus menunjukkan dua jalan keluar yang sah. Jalan pertama adalah trait object Box<dyn Ringkas> sebagaimana disarankan bagian help, yang mengizinkan beberapa tipe berbeda dengan pengarahan pemanggilan saat runtime. Jalan kedua adalah enum pembungkus dengan satu variant per tipe, sehingga tipe hasil tetap tunggal dan pemilihan variant dilakukan lewat match. Kedua pola tersebut mengganti impl Trait karena kebutuhannya sudah melampaui satu tipe konkret.

Kesalahan umum#

Kesalahan pertama adalah menganggap impl Trait pada dua parameter sebagai tipe yang sama. Setiap parameter impl Trait adalah parameter tipe anonim tersendiri, sehingga fn pindah(a: impl Ringkas, b: impl Ringkas) menerima dua argumen dengan tipe yang boleh berbeda. Apabila kedua argumen harus bertipe sama, gunakan bentuk generik bernama fn pindah<T: Ringkas>(a: T, b: T) yang mengikat satu nama T untuk keduanya.

Kesalahan kedua adalah memakai impl Trait pada tanda tangan publik pustaka tanpa mempertimbangkan keterbacaan API. Tanda tangan -> impl Ringkas tidak memberi tahu pemakai pustaka tipe konkret yang dihasilkan, sehingga dokumentasi menjadi kurang informatif dibandingkan nama tipe eksplisit. Pada modul internal atau binary, penyembunyian semacam ini justru menguntungkan karena tipe internal bebas diubah tanpa menyentuh pemanggil.

Kesalahan ketiga adalah mencoba mengembalikan referensi dari fungsi dengan nilai kembalian -> impl Trait tanpa memikirkan masa hidupnya. Nilai kembalian yang menumpang pada argumen tetap memerlukan anotasi lifetime seperti pada fungsi biasa, dan impl Trait tidak menghapus kewajiban tersebut.

Ringkasan#

  • Pada posisi argumen, impl Trait adalah singkatan fungsi generik dengan batasan, dan setiap parameternya merupakan parameter tipe anonim yang berdiri sendiri.
  • Pada posisi nilai kembalian, -> impl Trait menyembunyikan satu tipe konkret dari tanda tangan, dan pemanggilan metodenya tetap diarahkan saat kompilasi.
  • Satu fungsi hanya boleh mengembalikan satu tipe konkret, sehingga cabang dengan dua tipe berbeda ditolak dengan error E0308.
  • Kebutuhan beberapa tipe pada satu nilai kembalian dijawab oleh trait object Box<dyn Trait> atau enum pembungkus, bukan oleh impl Trait.

Referensi#