Parameter Tipe

Fungsi generik dan instansiasi.

Daftar isi

Parameter tipe (type parameter) memungkinkan satu fungsi melayani banyak tipe data dengan pemeriksaan yang tetap dilakukan saat kompilasi. Topik ini membahas cara mendeklarasikan parameter tipe pada fungsi, cara mengisinya saat pemanggilan, serta cara kompilator menyimpulkan tipe dari argumen. Setelah membacanya, fungsi umum seperti penjumlahan nilai dan pencarian elemen daftar dapat ditulis satu kali untuk semua tipe yang relevan tanpa duplikasi kode.

Masalah yang diselesaikan parameter tipe#

Sebelum parameter tipe tersedia, satu logika yang berlaku untuk banyak tipe hanya bisa ditulis dengan dua cara, dan keduanya menimbulkan biaya. Cara pertama adalah menduplikasi fungsi per tipe, misalnya satu fungsi penjumlahan untuk []int dan satu lagi untuk []float64. Setiap perbaikan pada logika harus diulang pada setiap salinannya, sehingga kode rawan kehilangan keserasian. Cara kedua adalah menerima parameter bertipe any, yaitu interface kosong yang menampung nilai apa pun. Informasi tipe hilang di dalam fungsi, sehingga setiap pemakaian kembali menuntut type assertion. Kesalahan tipe pun baru muncul dalam bentuk error saat program berjalan. Kedua cara itu juga memindahkan tanggung jawab ke tempat yang salah, yaitu ke pemanggil yang harus mengingat tipe aslinya.

Generics, yaitu parameter tipe yang diperiksa saat kompilasi dan tersedia sejak Go 1.18, memberikan jalan tengah untuk persoalan ini. Fungsi ditulis satu kali dengan parameter tipe, sedangkan setiap pemanggilan menetapkan tipe konkretnya. Kompilator menolak pemanggilan yang argumennya tidak sesuai, dan badan fungsi tetap mengetahui tipe yang sedang dipakai sehingga operasi seperti penjumlahan dapat ditulis tanpa asersi sama sekali.

Mendeklarasikan parameter tipe#

Deklarasi parameter tipe ditulis di antara tanda kurung siku tepat setelah nama fungsi dan sebelum kurung parameter biasa. Nama parameter tipe lazimnya satu huruf besar seperti T, diikuti constraint, yaitu daftar tipe yang diizinkan untuk parameter tersebut. Pada contoh berikut, constraint int | float64 menyatakan bahwa T hanya boleh berupa int atau float64, sedangkan di dalam badan fungsi T dipakai sebagaimana tipe biasa.

go rekap-nilai.go
package main

import "fmt"

// maksimum mengembalikan nilai yang lebih besar dari dua argumennya.
func maksimum[T int | float64](a, b T) T {
	if b > a {
		return b
	}
	return a
}

// jumlah menjumlahkan seluruh elemen slice nilai.
func jumlah[T int | float64](nilai []T) T {
	var total T
	for _, n := range nilai {
		total += n
	}
	return total
}

func main() {
	skor := []int{78, 91, 66, 84}
	bobot := []float64{0.25, 0.35, 0.15, 0.25}

	fmt.Println(maksimum(78, 91))
	fmt.Println(jumlah[int](skor))
	fmt.Println(jumlah(bobot))
}
text Keluaran
91
319
1

Fungsi jumlah mendeklarasikan var total T, dan kompilator mengisinya dengan nilai nol sesuai tipe yang dipilih untuk T saat pemanggilan, yaitu 0 untuk int dan 0.0 untuk float64. Operator += pada total += n sah karena kedua anggota constraint menyediakan operasi tersebut. Bila constraint memuat tipe yang tidak mendukung penjumlahan, baris tersebut langsung ditolak saat kompilasi tanpa perlu menjalankan programnya. Pemeriksaan semacam ini membuat kesalahan terdeteksi pada titik yang paling murah, yaitu sebelum program berjalan. Nama parameter tipe bebas dipilih sebagaimana nama variabel, dan lebih dari satu parameter tipe ditulis berdampingan di dalam kurung siku yang sama. Konvensi yang lazim adalah huruf T untuk tipe umum, sedangkan pasangan kunci dan nilai memakai K dan V.

Instansiasi dan inferensi tipe#

Proses mengganti parameter tipe dengan tipe konkret disebut instansiasi (instantiation). Argumen tipe dapat ditulis eksplisit seperti pada jumlah[int](skor), atau disimpulkan dari argumen fungsi seperti pada jumlah(bobot). Kompilator menyimpulkan T dengan mencocokkan tipe argumen terhadap parameter tipe fungsi, dan konstanta tanpa tipe seperti 78 ikut memandu penyimpulan karena dapat mewakili banyak tipe.

Satu fungsi generik boleh dipanggil dengan tipe berbeda pada tiap pemanggilan, karena setiap pemanggilan membentuk instansiasinya sendiri. Argumen tipe perlu dituliskan eksplisit pada pemanggilan yang argumennya tidak memberi petunjuk, misalnya fungsi tanpa parameter bertipe T. Tanpa petunjuk tersebut, kompilator menolak pemanggilan dan mengharuskan argumen tipe ditulis.

go cari-indeks.go
package main

import "fmt"

// indeksDari mengembalikan posisi pertama target dalam daftar, atau -1 bila tidak ada.
func indeksDari[T comparable](daftar []T, target T) int {
	for i, v := range daftar {
		if v == target {
			return i
		}
	}
	return -1
}

func main() {
	mataUji := []string{"aljabar", "statistika", "fisika"}
	bobot := []int{30, 45, 25}

	fmt.Println(indeksDari(mataUji, "statistika"))
	fmt.Println(indeksDari(mataUji, "kimia"))
	fmt.Println(indeksDari(bobot, 45))
	fmt.Println(indeksDari(bobot, 50))
}
text Keluaran
1
-1
1
-1

Fungsi indeksDari dipakai untuk daftar string dan daftar int dari definisi yang sama. Constraint comparable pada deklarasi fungsinya menuntut tipe yang mendukung operator == dan !=, yaitu syarat bagi pembandingan v == target di dalam badan fungsi. Pembahasan constraint yang lebih rinci ada pada topik Constraint. Kedua program pada topik ini dapat dijalankan lewat terminal sebagaimana berikut.

bash
# siapkan folder kerja untuk eksperimen topik ini
mkdir -p /tmp/generics/parameter-tipe
cp rekap-nilai.go cari-indeks.go /tmp/generics/parameter-tipe/
cd /tmp/generics/parameter-tipe

# pastikan tidak ada berkas yang keluar dari format gofmt
gofmt -l .

# jalankan kedua programnya
go run rekap-nilai.go
go run cari-indeks.go
text Keluaran
91
319
1
1
-1
1
-1

Perintah gofmt -l . tidak mencetak apa pun ketika seluruh berkas sudah sesuai format, sehingga keluaran yang tercetak seluruhnya berasal dari kedua program.

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul saat parameter tipe baru dipraktikkan.

  • Mengirim argumen di luar constraint. Pemanggilan jumlah([]string{"aljabar", "statistika"}) ditolak kompilator dengan pesan string does not satisfy int | float64 (string missing in int | float64). Perbaikannya, tambahkan tipe tersebut ke constraint bila memang dikehendaki, atau kirim argumen dengan tipe yang sudah tercantum.
  • Mencampur dua tipe pada satu pemanggilan. Pemanggilan maksimum(a, b) dengan a bertipe int dan b bertipe float64 ditolak dengan pesan in call to maksimum, type float64 of b does not match inferred type int for T, karena satu parameter tipe hanya menerima satu tipe per pemanggilan. Perbaikannya, konversi salah satu argumen terlebih dahulu agar tipenya sama.
  • Mengira parameter tipe menampung nilai bercampur tipe. Satu pemanggilan fungsi generik menampung satu tipe, bukan kumpulan tipe sekaligus. Untuk data yang isinya memang bercampur, gunakan []any beserta asersinya atau tinjau kembali desain datanya.

Ringkasan#

  • Parameter tipe ditulis di antara tanda kurung siku setelah nama fungsi dan diikuti constraint yang membatasi tipe yang diizinkan.
  • Setiap pemanggilan membentuk satu instansiasi dengan satu tipe konkret, sehingga definisi yang sama melayani banyak tipe tanpa duplikasi.
  • Argumen tipe dapat ditulis eksplisit atau disimpulkan dari argumen fungsi, dan konstanta tanpa tipe ikut memandu penyimpulan.
  • Pemanggilan yang argumennya di luar constraint ditolak saat kompilasi, bukan saat program berjalan.

Referensi#