Dua chapter sebelumnya menempatkan impl Trait di posisi masuk — parameter. Kata kunci yang sama juga bisa duduk di posisi keluar: fn make_article() -> impl Summary menyatakan “fungsi ini mengembalikan suatu tipe konkret, dan cukup bagi kita bahwa tipe tersebut mengimplementasikan Summary”. Manfaatnya bagi perancang API nyata: tipe internal yang dipilih untuk nilai kembalian — struct dengan lima field, misalnya — tidak perlu dipublikasikan sebagai bagian kontrak fungsi. Pemanggil cukup mengandalkan method yang dijanjikan trait, dan perancang bebas mengganti tipe konkret di baliknya tanpa mengubah halaman dokumentasi. Seperti biasa di Rust, keringkasan ini tidak mengorbankan kecepatan: nilai yang kembali tetap berupa tipe konkret berukuran tetap yang diketahui penuh oleh kompilator, bukan kotak ajaib. Batasnya juga tegas: satu signature impl Trait hanya boleh mengembalikan satu tipe konkret — dan batas itulah sumber kebingungan paling umum.

Mengembalikan Tipe yang Dijaga Abstrak #

Sebelum masuk ke kode, satu perbedaan arah perlu dipertegas. Pada chapter sebelumnya, impl Summary di posisi argumen berarti “pemanggil yang memilih tipe apa pun asalkan memenuhi kontrak” — fleksibilitas ada di sisi pemanggil. Di posisi kembalian, maknanya terbalik: “fungsi inilah yang memilih satu tipe konkret, dan pemanggil hanya diberi tahu bahwa tipe itu memenuhi kontrak”. Keputusan tetap ada di sisi perancang fungsi, bukan pemanggil. Pembalikan arah ini yang membuat dua posisi impl Trait tidak saling menggantikan meski penulisannya mirip.

Pabrik konten sederhana kita menghasilkan artikel jadi dengan penulis dan lokasi baku. Tipe konkret yang dikembalikan adalah NewsArticle, tetapi signature hanya mengumumkan impl Summary:

rust
trait Summary {
    fn summarize(&self) -> String;
}

struct NewsArticle {
    author: String,
    location: String,
    headline: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, {} ({})", self.headline, self.location, self.author)
    }
}

fn make_article(author: String, headline: String) -> impl Summary {
    NewsArticle {
        author,
        location: String::from("Jakarta"),
        headline,
    }
}

fn main() {
    let item = make_article(
        String::from("Rina Wijaya"),
        String::from("Kereta Cepat Jalur Utama Mulai Diuji"),
    );

    println!("Item yang diterima: {}", item.summarize());
}

Output yang Dihasilkan:

text
Item yang diterima: Kereta Cepat Jalur Utama Mulai Diuji, Jakarta (Rina Wijaya)

Mari kita bedah kode di atas:

  1. Baris 17 (fn make_article(author: String, headline: String) -> impl Summary): tipe kembalian impl Summary menyatakan janji kemampuan, bukan identitas. Kompilator tetap tahu tipe sebenarnya — kompilator membaca body fungsi dan menyimpulkan NewsArticle — tetapi informasi itu tidak bocor ke luar signature.
  2. Baris 18-22 (NewsArticle { ... }): body mengembalikan NewsArticle secara eksplisit. Singkatan penulisan field author, dan headline, dipakai karena nama variabel dan nama field sama — kebiasaan dari section Struktur Data.
  3. Baris 26-29 (let item = make_article(...);): variabel item menampung hasil panggilan pabrik. Nilainya tetap NewsArticle utuh di memori — tidak ada pembungkus, tidak ada penunjuk tambahan.
  4. Baris 31 (println!("Item yang diterima: {}", item.summarize());): pemanggil hanya memakai method dari Summary. Pemanggilan tetap di-resolve secara statis ke impl Summary for NewsArticle karena kompilator memegang tipe konkret tersebut secara internal.

Pemanggil Hanya Melihat Kontrak #

Sisi lain dari pengabstraksian ini: field milik tipe konkret berhenti terlihat dari luar. Pemanggil yang mencoba mengakses headline langsung dari nilai kembalian akan ditolak:

rust
trait Summary {
    fn summarize(&self) -> String;
}

struct NewsArticle {
    author: String,
    location: String,
    headline: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, {} ({})", self.headline, self.location, self.author)
    }
}

fn make_article(headline: String) -> impl Summary {
    NewsArticle {
        author: String::from("Rina Wijaya"),
        location: String::from("Jakarta"),
        headline,
    }
}

fn main() {
    let item = make_article(String::from("Kereta Cepat Jalur Utama Mulai Diuji"));
    println!("Kepala berita: {}", item.headline);
}

Pesan error kompilator:

text
error[E0609]: no field `headline` on type `impl Summary`
  --> main.rs:27:40
   |
27 |     println!("Kepala berita: {}", item.headline);
   |                                        ^^^^^^^^ unknown field

error: aborting due to 1 previous error

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

Pesan E0609 menyebut tipe impl Summary sebagai tipe variabel item — dari sudut pandang pemanggil, itulah satu-satunya identitas yang ada. Field headline memang ada pada NewsArticle, tetapi pengabstraksian return menyembunyikan bentuk data di balik janji kemampuan. Penolakan ini bukan kelemahan melainkan daya tahan desain: jika kelak kita mengganti make_article agar membangun tipe internal baru — misalnya WireNews dengan struktur field berbeda — seluruh pemanggil yang hanya memakai summarize() tetap dikompilasi ulang tanpa perubahan. Data yang benar-benar dibutuhkan pemanggil sebaiknya dikeluarkan lewat method kontrak, bukan dibaca dari field.

Manfaat ini paling terasa pada kode yang tidak kita miliki. Ketika fungsi pustaka mengumumkan -> impl Iterator, kita sebagai pemanggil tidak perlu tahu struct sintetis apa yang dikembalikan di baliknya — cukup method next yang dijanjikan trait. Detail tipe internal pustaka tetap boleh berubah antar versi tanpa merusak kode kita, selama janji trait-nya dijaga. Kebalikannya juga berguna bagi pustaka kita sendiri: tipe pembantu sementara yang tidak layak didokumentasikan bisa keluar dari fungsi publik tanpa nama yang perlu dirawat.

Satu Signature, Satu Tipe Konkret #

Batas penting impl Trait pada posisi return: seluruh jalur keluar fungsi harus menghasilkan tipe konkret yang sama. Kompilator mengikat satu tipe anonim per signature, sehingga percabangan yang mengembalikan dua tipe berbeda ditolak:

rust
trait Summary {
    fn summarize(&self) -> String;
}

struct NewsArticle {
    author: String,
    headline: String,
}

struct Tweet {
    username: String,
    content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}: {}", self.author, self.headline)
    }
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

fn make_summary(is_news: bool) -> impl Summary {
    if is_news {
        NewsArticle {
            author: String::from("Rina Wijaya"),
            headline: String::from("Rupiah Menguat Tipis"),
        }
    } else {
        Tweet {
            username: String::from("budi_perjalanan"),
            content: String::from("Cuaca sore cerah sekali."),
        }
    }
}

fn main() {
    let item = make_summary(true);
    println!("{}", item.summarize());
}

Pesan error kompilator:

text
error[E0308]: `if` and `else` have incompatible types
  --> main.rs:34:9
   |
28 | /       if is_news {
29 | | /         NewsArticle {
30 | | |             author: String::from("Rina Wijaya"),
31 | | |             headline: String::from("Rupiah Menguat Tipis"),
32 | | |         }
   | | |_________- expected because of this
33 | |       } else {
34 | | /         Tweet {
35 | | |             username: String::from("budi_perjalanan"),
36 | | |             content: String::from("Cuaca sore cerah sekali."),
37 | | |         }
   | | |_________^ expected `NewsArticle`, found `Tweet`
38 | |       }
   | |_______- `if` and `else` have incompatible types
   |
help: you could change the return type to be a boxed trait object
   |
27 - fn make_summary(is_news: bool) -> impl Summary {
27 + fn make_summary(is_news: bool) -> Box<dyn Summary> {
   |
help: if you change the return type to expect trait objects, box the returned expressions
   |
29 ~         Box::new(NewsArticle {
30 |             author: String::from("Rina Wijaya"),
31 |             headline: String::from("Rupiah Menguat Tipis"),
32 ~         })
33 |     } else {
34 ~         Box::new(Tweet {
35 |             username: String::from("budi_perjalanan"),
36 |             content: String::from("Cuaca sore cerah sekali."),
37 ~         })
   |

error: aborting due to 1 previous error

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

Pesan E0308 menegaskan aturannya dari sisi ekspresi: cabang if menghasilkan NewsArticle sehingga cabang else wajib menghasilkan tipe yang sama, dan Tweet gagal. Kompilator bahkan menawarkan jalan keluar yang justru membuka mata kita — bantuan pertama menyarankan mengubah tipe kembalian menjadi Box<dyn Summary>, bentuk trait object yang menampung tipe berbeda dengan dynamic dispatch. Itu topik section Polimorfisme: Box<dyn Summary> memungkinkan satu signature menampung banyak tipe konkret, dengan biaya alokasi heap dan pemilihan method saat runtime. Selama kebutuhan kita satu-tipe-per-signature — yang merupakan mayoritas kasus, misalnya rantai iterator yang dibahas section Iterator — impl Trait memberikan abstraksi tanpa biaya tersebut.

Mengapa batas sekeras itu? Karena nilai kembalian impl Summary tetap harus punya ukuran dan bentuk memori yang pasti: kompilator menghimpun pemanggil make_summary dengan asumsi satu tipe konkret di balik janji tersebut. Dua cabang dengan dua tipe berbeda berarti dua bentuk memori berbeda — NewsArticle dan Tweet punya jumlah field berbeda — dan pemanggil tidak bisa menyediakan satu slot penyimpanan untuk dua bentuk sekaligus. Box<dyn Summary> memecahkan persoalan itu dengan menyimpan nilai di heap dan hanya mengembalikan penunjuk berukuran tetap; penundaan keputusan inilah harga yang dibayarnya.

Ringkasan #

impl Trait pada posisi return menyembunyikan tipe konkret di balik janji kemampuan: kompilator tetap menyimpulkan dan mengikat tipe tersebut secara internal, pemanggil hanya melihat method kontrak, dan akses langsung ke field ditolak dengan E0609. Harga dari kejernihan ini adalah ketatnya aturan satu signature satu tipe — cabang return dengan tipe berbeda ditolak E0308, dan kompilator sendiri mengarahkan kita ke Box<dyn Summary> saat kebutuhan memang bercabang-tipe. Perkakas trait kita kini lengkap dari definisi hingga penggunaan di parameter dan kembalian. Sebelum melangkah ke penerapan lanjutan, ada satu keluarga trait yang membuat hidup kita jauh lebih ringkas: trait yang implementasinya ditulis otomatis oleh kompilator lewat derive.