Interface dan Impl

Constraint bernama melalui interface dan impl yang diperiksa pada batas pemanggilan.

Daftar isi

Topik ini membahas dua sisi constraint bernama pada generics Carbon, yaitu interface yang mendeklarasikan kemampuan dan impl yang mengaitkan kemampuan itu dengan sebuah tipe. Pembahasan bergerak dari definisi keduanya, program dua bentuk geometri yang dapat dieksekusi di Explorer, hingga pemeriksaan yang terjadi pada batas pemanggilan fungsi generik. Setelah membacanya, alur pengecekan dari deklarasi interface sampai pemanggilan yang ditolak dapat diikuti lengkap dengan bukti eksekusinya.

Interface sebagai kontrak bernama#

Interface adalah constraint bernama untuk parameter waktu kompilasi. Dokumen desain generics mendefinisikannya sebagai sebuah nama yang mendeskripsikan fungsi, metode, dan entitas lain yang dapat diimplementasikan oleh tipe. Deklarasinya menggunakan kata kunci interface, dan setiap fungsi di dalamnya menyebut parameter Self sebagai tempat tipe yang mengimplementasikan interface tersebut. Sifatnya nominal, yaitu sebuah tipe tidak otomatis memenuhi interface hanya karena kebetulan memiliki fungsi dengan nama yang sama, melainkan harus menyatakan implementasinya secara eksplisit melalui impl.

Dokumen desain juga mengatur dua kata kunci tambahan untuk fungsi di dalam interface. Deklarasi yang diberi awalan default boleh membawa tubuh berisi implementasi bawaan, sedangkan final melarang implementasi itu ditimpa oleh tipe yang mengimplementasikannya. Interface hanya mendeskripsikan kemampuan dan tidak menyimpan data, sehingga deklarasi variabel tidak diperbolehkan di dalamnya. Bentuk berikut memperlihatkan interface yang digunakan sepanjang topik ini.

carbon Interface Luas sebagaimana ditulis dokumen desain
// tidak-dijalankan
interface Luas {
  fn Hitung(self: Self) -> i32;
}

Interface Luas menuntut satu fungsi Hitung yang menerima Self dan mengembalikan i32. Pemilihan i32 mengikuti batasan interpreter Explorer pada trunk yang baru mendukung i32 untuk angka, sehingga contoh pada topik ini dapat dieksekusi apa adanya. Rancangan lengkapnya jauh lebih luas, misalnya associated constants dan interface berparameter yang dijelaskan dokumen desain, tetapi keduanya tidak diperlukan untuk membaca contoh di sini.

Impl sebagai jangkar implementasi#

Impl adalah deklarasi yang mengaitkan sebuah tipe dengan sebuah interface sekaligus mengisi tubuh setiap fungsi yang dituntutnya. Bentuk umumnya impl NamaTipe as NamaInterface, diikuti definisi fungsi-fungsi interface tersebut. Dokumen desain mengizinkan impl ditulis di dalam tubuh kelas maupun di luar kelas, dan pada rancangan lengkapnya impl di luar kelas hanya boleh berada di library yang mendefinisikan salah satu nama dari type structure-nya, yaitu pola tipe yang ditulis pada deklarasi impl. Aturan itu mengarah pada sifat coherence agar setiap pasangan tipe dan interface memiliki satu implementasi yang sah.

Program berikut menggunakan dua tipe, yaitu Persegi dan PersegiPanjang, yang keduanya mengimplementasikan Luas dengan rumus berbeda. Fungsi generik CetakLuas menerima satu parameter bentuk yang tipenya ditulis T dan diperiksa terhadap interface Luas.

carbon dua-impl.carbon
package sample api;

interface Luas {
  fn Hitung(self: Self) -> i32;
}

class Persegi {
  var sisi: i32;
}

class PersegiPanjang {
  var panjang: i32;
  var lebar: i32;
}

impl Persegi as Luas {
  fn Hitung(self: Self) -> i32 {
    return self.sisi * self.sisi;
  }
}

impl PersegiPanjang as Luas {
  fn Hitung(self: Self) -> i32 {
    return self.panjang * self.lebar;
  }
}

fn CetakLuas[T:! Luas](bentuk: T) {
  Print("Luas = {0}", T.Hitung(bentuk));
}

fn Main() -> i32 {
  var p: Persegi = {.sisi = 4};
  var pp: PersegiPanjang = {.panjang = 3, .lebar = 5};
  CetakLuas(p);
  CetakLuas(pp);
  return 0;
}
text Keluaran
Luas = 16
Luas = 15
result: 0

Keluaran memuat dua angka luas pada baris berurutan, karena Compiler Explorer menampilkan setiap pemanggilan Print sebagai satu baris dan menambahkan baris result: 0 sebagai kode kembalian Main. Nilai 16 berasal dari impl Persegi dan nilai 15 berasal dari impl PersegiPanjang. Kedua pemanggilan CetakLuas menjalankan tubuh fungsi yang sama persis, sedangkan perbedaan rumus seluruhnya berasal dari impl yang ditemukan untuk T pada tiap pemanggilan.

Pemeriksaan pada batas pemanggilan#

Pola [T:! Luas] pada deklarasi CetakLuas merupakan checked binding, yaitu penanda bahwa T adalah parameter generik yang diperiksa. Tubuh fungsi diperiksa sekali pada saat definisi, tanpa mengetahui tipe argumen yang akan datang, dan pemeriksaannya cukup terhadap nama-nama yang dijanjikan interface. Pada saat pemanggilan, nilai T dideduksi dari tipe argumen, kemudian pemeriksa mencari impl tipe itu terhadap Luas sebelum program dijalankan. Pemanggilan hanya sah bila impl ditemukan, sehingga batas pemanggilan menjadi titik pemeriksaan utama program generik.

Pemanggilanfungsi generikT dideduksidari tipe argumenCari implT as Luasimpl ditemukan?tidakError: impl tidak ditemukanyaPanggilan sah,T.Hitung dieksekusi
Pemeriksaan impl pada batas pemanggilan fungsi generik

Percobaan berikut menyajikan versi tanpa impl untuk Segitiga, lalu tetap memanggil CetakLuas dengan argumen Segitiga. Pemanggilan ditolak pada batas pemanggilan dengan pesan yang menyebut pasangan interface dan tipe yang bermasalah.

carbon tanpa-impl.carbon
package sample api;

interface Luas {
  fn Hitung(self: Self) -> i32;
}

class Segitiga {
  var alas: i32;
  var tinggi: i32;
}

fn CetakLuas[T:! Luas](bentuk: T) {
  Print("Luas = {0}", T.Hitung(bentuk));
}

fn Main() -> i32 {
  var t: Segitiga = {.alas = 6, .tinggi = 4};
  CetakLuas(t);
  return 0;
}
text Keluaran
COMPILATION ERROR: <source>:18: could not find implementation of interface Luas for class Segitiga

Pesan error tersebut memperlihatkan keunggulan model terperiksa. Kesalahan terdeteksi pada berkas pemanggil, bukan jauh di dalam tubuh fungsi yang telah diinstansiasi, dan pesannya menunjuk pasangan interface serta tipe yang belum terhubung secara langsung. Apabila impl untuk Segitiga ditambahkan, pemanggilan yang sama akan diterima tanpa mengubah CetakLuas sama sekali.

Batas interpreter juga perlu dinyatakan apa adanya. Pemanggilan p.Hitung() pada objek Persegi ditolak oleh interpreter, sebagaimana program berikut.

carbon metode-langsung.carbon
package sample api;

interface Luas {
  fn Hitung(self: Self) -> i32;
}

class Persegi {
  var sisi: i32;
}

impl Persegi as Luas {
  fn Hitung(self: Self) -> i32 {
    return self.sisi * self.sisi;
  }
}

fn Main() -> i32 {
  var p: Persegi = {.sisi = 4};
  p.Hitung();
  return 0;
}
text Keluaran
COMPILATION ERROR: <source>:19: class Persegi does not have a field named Hitung

Dokumen desain mengatur dua bentuk pemanggilan metode interface, yaitu simple member access untuk impl yang dinyatakan dengan extend di dalam kelas dan qualified member access seperti p.(Luas.Hitung)() untuk semua impl. Keduanya belum dijalankan oleh interpreter Explorer, sehingga pola yang berjalan saat ini adalah T.Hitung(bentuk) di dalam fungsi generik yang parameternya diperiksa terhadap interface tersebut. Penyebutan Luas.Hitung(p) di luar pola itu juga ditolak dengan pesan expected callee to be a function, found member name Hitung.

Ringkasan#

  • Interface adalah constraint bernama yang mendeklarasikan fungsi untuk tipe, bersifat nominal, dan tidak menyimpan data.
  • Impl mengaitkan tipe dengan interface beserta tubuh fungsinya, dan satu pasangan tipe serta interface hanya boleh dideklarasikan sekali.
  • Binding [T:! Luas] membuat tubuh fungsi diperiksa sekali pada definisi, sedangkan impl dicari pada batas pemanggilan dari tipe argumen.
  • Di Explorer, T.Hitung(bentuk) di dalam fungsi generik berjalan, sedangkan p.Hitung() dan Luas.Hitung(p) belum didukung interpreter.

Referensi#