Generics

Proc dan tipe parametrik beserta inferensi tipe pada waktu kompilasi.

Daftar isi

Proc yang sama sering ditulis berulang untuk tipe yang berbeda, misalnya pencari nilai terbesar untuk int, float, dan string. Topik ini membahas generics, yaitu proc dan tipe yang diparameterisasi dengan parameter tipe sehingga satu definisi melayani banyak tipe konkret. Setelah membacanya, duplikasi proc penunjang dapat dihapus, dan perilaku generics Nim pada waktu kompilasi dapat dijelaskan dengan tepat.

Proc generik dan inferensi tipe#

Parameter tipe ditulis dalam kurung siku setelah nama proc, misalnya maksimum[T]. Di dalam tubuh proc, T digunakan seperti tipe biasa untuk parameter, nilai kembalian, maupun variabel lokal. Tipe konkretnya tidak ditetapkan saat definisi, melainkan saat pemanggilan dari tipe argumennya, mekanisme yang disebut inferensi tipe (type inference).

nim maksimum.nim
# Proc generik dengan inferensi tipe
proc maksimum[T](a, b: T): T =
  if a < b:
    result = b
  else:
    result = a

echo maksimum(3, 7)
echo maksimum(2.5, 1.8)
echo maksimum("jeruk", "apel")
echo maksimum[int](2, 9)
text Keluaran
7
2.5
jeruk
9

Tiga pemanggilan pertama menyimpulkan T sendiri, yaitu int, float, lalu string, tanpa satu pun anotasi tambahan. Pemanggilan keempat menuliskan instansiasi eksplisit maksimum[int] yang mengunci T, dan bentuk ini digunakan bila inferensi tidak mungkin atau kejelasan lebih diutamakan. Syarat kerjanya berupa operator < yang tersedia untuk T, karena tubuh proc memanggilnya, sehingga maksimum bekerja untuk semua tipe yang mendukung perbandingan tersebut.

Inferensi juga punya batas yang tegas, yaitu kedua argumen harus mengarah ke satu T yang sama.

nim Contoh: inferensi yang gagal
# tidak-dijalankan
# kedua argumen bertipe berbeda sehingga T tidak dapat disimpulkan
echo maksimum(3, 4.5)

Kompilator menolak potongan di atas dengan pesan Error: type mismatch untuk Expression: maksimum(3, 4.5), lalu mencantumkan [1] 3: int literal(3) dan [2] 4.5: float64. Tidak ada satu T yang cocok untuk keduanya, sehingga pencocokan berhenti. Perbaikannya berupa konversi eksplisit salah satu argumen, misalnya maksimum(3.0, 4.5).

Batas atas tipe yang disimpulkan juga dapat dinyatakan lewat batasan tipe (type constraint). Deklarasi proc maksimum[T: Ordinal](a, b: T): T membatasi T pada tipe ordinal, sehingga pemanggilan maksimum(3, 7) tetap sah, sedangkan maksimum("b", "a") ditolak karena string bukan tipe ordinal. Batasan seperti ini mendokumentasikan syarat proc pada tempat yang diperiksa kompilator, bukan hanya dalam komentar.

Tipe parametrik dengan Tumpukan[T]#

Tipe dapat diparameterisasi dengan cara yang sama seperti proc. Deklarasi Tumpukan[T] menggunakan seq[T] sebagai penyimpanannya, dan setiap proc pendamping membawa [T] sendiri agar cocok dengan tumpukan yang dilayaninya. Instansiasi tertulis pada deklarasi variabel, baik eksplisit seperti var angka: Tumpukan[int] maupun lewat konstruktor seperti Tumpukan[string]().

nim tumpukan.nim
# Tipe generik Tumpukan[T]
type
  Tumpukan[T] = object
    isi: seq[T]

proc dorong[T](t: var Tumpukan[T], nilai: T) =
  t.isi.add(nilai)

proc tarik[T](t: var Tumpukan[T]): T =
  result = t.isi[^1]
  t.isi.setLen(t.isi.len - 1)

proc ukuran[T](t: Tumpukan[T]): int =
  t.isi.len

var angka: Tumpukan[int]
dorong(angka, 10)
dorong(angka, 25)
dorong(angka, 40)

echo angka.ukuran
echo angka.tarik()
echo angka.ukuran

var nama = Tumpukan[string]()
dorong(nama, "Ali")
dorong(nama, "Citra")
echo nama.tarik()
text Keluaran
3
40
2
Citra

Dua tumpukan pada program di atas adalah dua tipe konkret yang berbeda, sehingga mendorong string ke Tumpukan[int] ditolak saat kompilasi. Pemanggilan angka.tarik() menggunakan sintaks panggilan metode yang setara dengan tarik(angka), dan tarik dinyatakan dengan parameter var karena mengubah isi tumpukannya. Memanggil tarik pada tumpukan kosong memicu IndexDefect dengan pesan index out of bounds, the container is empty, sehingga pemakaian nyata memeriksa ukuran terlebih dahulu. Kode generik Nim diwujudkan dengan monomorfisasi, yaitu kompilator menyusun satu salinan kode khusus untuk setiap kombinasi tipe yang benar-benar dipakai, sehingga tidak ada pemilihan tipe saat program berjalan dan tidak ada biaya tambahan dibanding proc yang ditulis manual untuk tiap tipe.

Proc pendamping tipe generik sendiri diinstansiasi mengikuti pemakaian, sehingga program di atas menghasilkan versi int dan string hanya untuk proc yang benar-benar dipanggil, tanpa satu pun duplikasi pada kode sumber. Iterator dan proc biasa dapat diparameterisasi dengan cara yang sama, sebagaimana dicatat dokumentasi tutorial Nim bagian kedua.

Evaluasi pada waktu kompilasi#

Generics diselesaikan pada waktu kompilasi, bukan saat program berjalan. Setiap kombinasi parameter tipe yang digunakan menghasilkan satu versi proc atau tipe yang dibangun khusus untuk tipe konkretnya, dan contoh-contoh pada topik ini menghasilkan versi int, float, serta string sesuai pemakaiannya. Nim tidak menerapkan type erasure, yaitu penghapusan informasi tipe yang membuat semua instansiasi berbagi satu implementasi bertipe samar.

Detail praktisnya terasa pada pesan error. Apabila tubuh proc memanggil operator yang belum tersedia untuk salah satu tipe instansiasi, kesalahan dilaporkan pada saat instansiasi tersebut, lengkap dengan tipe konkret yang sedang dibangun. Karena setiap versi berdiri sendiri, panggilannya juga berlangsung tanpa lapisan perantara saat runtime.

Konsekuensi lainnya berada di sisi biaya. Setiap instansiasi menambah kode yang dikompilasi, sehingga tipe generik yang diinstansiasi pada sangat banyak tipe memperbesar ukuran program. Bagi sebagian besar program, pemeriksaan tipe yang lengkap dan panggilan yang tersusun langsung tanpa lapisan perantara lebih berharga daripada penghematan ukuran biner tersebut.

Kesalahan umum#

Kesalahan pertama berupa mengharapkan satu proc generik menerima argumen bertipe campuran, sebagaimana contoh maksimum(3, 4.5) di atas, dan perbaikannya selalu berupa konversi eksplisit. Kesalahan kedua berupa menulis tubuh proc yang memanggil operasi khusus tipe tertentu pada T, karena pemeriksaan berlangsung saat instansiasi dan bergantung pada tipe yang benar-benar digunakan. Pemanggilan dengan argumen yang tidak memenuhi syarat tubuh proc, misalnya mendorong angka ke tumpukan string, ditolak dengan type mismatch yang menunjuk baris pemanggilannya.

Ringkasan#

  • Parameter tipe [T] memparameterisasi proc dan tipe, dan tipe konkretnya disimpulkan dari argumen pada pemanggilan.
  • Instansiasi eksplisit seperti maksimum[int](2, 9) mengunci T bila inferensi tidak mungkin atau kurang jelas.
  • Setiap instansiasi dibangun khusus pada waktu kompilasi tanpa type erasure, sehingga pemeriksaan tipe berlaku penuh dan error muncul sebelum program berjalan.
  • Tipe parametrik seperti Tumpukan[T] menggunakan parameter tipe yang sama pada field dan proc pendampingnya.

Referensi#