Constraint

any, comparable, dan interface constraint kustom.

Daftar isi

Constraint menetapkan himpunan tipe yang diizinkan untuk sebuah parameter tipe, sehingga badan fungsi mengetahui operasi yang boleh dipakai. Topik ini membahas constraint any dan comparable yang tersedia di pustaka standar, elemen union beserta tanda tilde untuk tipe dasar, serta penulisan interface constraint kustom yang menuntut metode tertentu. Setelah membacanya, pembatasan tipe pada fungsi generik dapat dipilih dan ditulis dengan alasan yang jelas.

Setiap constraint adalah interface#

Segala bentuk constraint pada Go berupa interface, yaitu daftar perilaku atau tipe yang dituntut dari parameter tipe. Constraint any adalah interface kosong yang tidak menuntut apa pun, sehingga semua tipe memenuhinya dan badan fungsi tidak boleh berasumsi tentang operasi yang tersedia. Bentuk constraint yang lebih berguna adalah daftar tipe yang dipisahkan tanda |, yang disebut elemen union (union). Union int | float64 menuntut T berupa int atau float64, dan operasi yang sah di dalam fungsi adalah operasi yang dimiliki kedua tipe itu sekaligus.

Fungsi berikut menghitung rerata untuk kumpulan int dan kumpulan float64 dari satu definisi. Konversi float64(n) dan penjumlahan total += sah karena kedua anggota union menyediakannya, sedangkan argumen bertipe lain ditolak saat kompilasi sebelum program sempat dijalankan. Pemeriksaan di titik ini jauh lebih murah daripada menemukan kesalahan tipe pada saat program berjalan.

go rerata-nilai.go
package main

import "fmt"

// rataRata menghitung rerata sekumpulan nilai bertipe int atau float64.
func rataRata[T int | float64](nilai []T) float64 {
	var total float64
	for _, n := range nilai {
		total += float64(n)
	}
	return total / float64(len(nilai))
}

func main() {
	poin := []int{70, 80, 90}
	berat := []float64{62.5, 71.0, 66.5}

	fmt.Println(rataRata(poin))
	fmt.Println(rataRata(berat))
}
text Keluaran
80
66.66666666666667

Pemanggilan rataRata(poin) membentuk instansiasi dengan T berupa int, sedangkan rataRata(berat) membentuk instansiasi dengan T berupa float64. Kedua pemanggilan berbagi definisi yang sama, tetapi pemeriksaan tipe masing-masing tetap dilakukan penuh saat kompilasi.

comparable untuk perbandingan kesamaan#

Constraint comparable menuntut tipe yang mendukung operator == dan !=. Tipe bilangan, string, bool, pointer, channel, serta array dan struct yang seluruh isinya dapat dibandingkan memenuhi syarat ini, sedangkan slice, map, dan tipe fungsi tidak. Tipe interface juga memenuhi comparable sejak Go 1.20, dengan catatan perbandingannya dapat memicu panic saat berjalan apabila nilai di dalamnya tidak dapat dibandingkan. Constraint ini juga menjadi syarat bagi tipe kunci sebuah map, sehingga fungsi generik yang menyentuh kunci map umumnya menuntut comparable.

Constraint any dan comparable tersedia tanpa pernyataan impor karena menjadi bagian dari pustaka bawaan Go dan didokumentasikan pada paket builtin. Pemilihan comparable juga membuat maksud fungsi terbaca, karena pembaca langsung tahu bahwa fungsi itu hanya membutuhkan kesamaan dan bukan perbandingan urutan maupun operasi lain. Constraint yang menuntut lebih banyak operasi mempersempit himpunan tipe yang memenuhinya, sehingga pilihan constraint selalu menyeimbangkan kemampuan yang dibutuhkan badan fungsi dan luas tipe yang ingin dilayani.

Pencarian kesamaan memang membutuhkan constraint comparable. Penulisan dengan any ditolak kompilator sebagaimana berikut.

go Pembandingan kesamaan yang ditolak pada constraint any
// tidak-dijalankan
func memuat[T any](daftar []T, target T) bool {
	for _, v := range daftar {
		if v == target {
			return true
		}
	}
	return false
}

Kompilator menolak baris v == target dengan pesan invalid operation: v == target (incomparable types in type set), karena any memuat tipe yang tidak mendukung perbandingan kesamaan. Dengan comparable, fungsi yang sama sah dan dapat dipakai untuk daftar string maupun daftar bilangan.

go cari-elemen.go
package main

import "fmt"

// memuat melaporkan apakah target ada dalam daftar.
func memuat[T comparable](daftar []T, target T) bool {
	for _, v := range daftar {
		if v == target {
			return true
		}
	}
	return false
}

// hitung menghitung banyaknya kemunculan target dalam daftar.
func hitung[T comparable](daftar []T, target T) int {
	n := 0
	for _, v := range daftar {
		if v == target {
			n++
		}
	}
	return n
}

func main() {
	nilai := []string{"A", "B", "A", "C", "A"}

	fmt.Println(memuat(nilai, "C"))
	fmt.Println(memuat(nilai, "D"))
	fmt.Println(hitung(nilai, "A"))
	fmt.Println(hitung([]int{7, 7, 3}, 7))
}
text Keluaran
true
false
3
2

Elemen aproksimasi untuk tipe dasar#

Union mencocokkan tipe secara persis. Tipe yang didefinisikan sendiri (defined type) seperti type Suhu float64 tidak termasuk ke dalam int | float64, sekalipun tipe dasarnya (underlying type) adalah float64. Pemanggilan rataRata([]Suhu{26.5, 27.0, 28.5}) pada fungsi bagian sebelumnya ditolak dengan pesan Suhu does not satisfy int | float64 (possibly missing ~ for float64 in int | float64).

Pesan tersebut sekaligus mengarahkan pada solusinya. Tanda tilde (~) pada elemen union menyatakan semua tipe yang tipe dasarnya cocok, dan bentuk ini disebut elemen aproksimasi (approximation element). Constraint ~int | ~float64 mencakup int, float64, serta tipe sendiri yang bertumpu pada keduanya, sehingga fungsi rataRata yang sama dapat dipakai untuk data sensor. Perluasan ini tidak mengubah badan fungsi sedikit pun, karena konversi float64(n) tetap sah untuk tipe apa pun yang tipe dasarnya float64. Elemen aproksimasi juga berguna untuk bentuk kumpulan lain, misalnya ~[]byte yang mencakup setiap tipe yang bertumpu pada []byte.

go rerata-suhu.go
package main

import "fmt"

// Suhu adalah tipe sendiri dengan tipe dasar float64.
type Suhu float64

// rataRata menerima tipe apa pun yang tipe dasarnya int atau float64.
func rataRata[T ~int | ~float64](nilai []T) float64 {
	var total float64
	for _, n := range nilai {
		total += float64(n)
	}
	return total / float64(len(nilai))
}

func main() {
	sensor := []Suhu{26.5, 27.0, 28.5}
	poin := []int{70, 80, 90}

	fmt.Println(rataRata(sensor))
	fmt.Println(rataRata(poin))
}
text Keluaran
27.333333333333332
80

Interface constraint kustom#

Constraint juga dapat menuntut metode. Bentuk ini cocok untuk fungsi generik yang memanggil perilaku tertentu dari setiap item tanpa memedulikan tipe konkretnya, dan penulisannya sama dengan deklarasi interface biasa. Constraint Penilai berikut menuntut metode Nilai yang mengembalikan float64. Dua struct yang saling tidak berkaitan tetap dapat diproses oleh fungsi rerata yang sama karena keduanya menyediakan metode tersebut. Bentuk ini memadukan penuntutan perilaku dengan pemeriksaan tipe saat kompilasi. Setiap item tetap bertipe konkretnya di dalam badan fungsi, sehingga pemanggilan x.Nilai() tidak memerlukan asersi.

go nilai-rerata.go
package main

import "fmt"

// Penilai adalah constraint untuk tipe yang menyediakan nilai numerik.
type Penilai interface {
	Nilai() float64
}

// MataKuliah menyimpan satu mata kuliah beserta skornya.
type MataKuliah struct {
	Nama string
	Skor float64
}

// Nilai mengembalikan skor mata kuliah.
func (m MataKuliah) Nilai() float64 {
	return m.Skor
}

// Latihan menyimpan satu latihan mandiri beserta skornya.
type Latihan struct {
	Topik string
	Skor  float64
}

// Nilai mengembalikan skor latihan.
func (l Latihan) Nilai() float64 {
	return l.Skor
}

// rerata menghitung rerata nilai dari sekumpulan item bertipe sama.
func rerata[T Penilai](item []T) float64 {
	var total float64
	for _, x := range item {
		total += x.Nilai()
	}
	return total / float64(len(item))
}

func main() {
	kuliah := []MataKuliah{
		{Nama: "aljabar", Skor: 82},
		{Nama: "statistika", Skor: 90},
	}
	fmt.Println(rerata(kuliah))

	latihan := []Latihan{
		{Topik: "perulangan", Skor: 75},
		{Topik: "fungsi", Skor: 88},
		{Topik: "struct", Skor: 91},
	}
	fmt.Println(rerata(latihan))
}
text Keluaran
86
84.66666666666667

Pemanggilan dengan tipe yang tidak menyediakan metode yang dituntut ditolak saat kompilasi. Struct SkorMentah tanpa metode Nilai menghasilkan pesan in call to rerata, T (type SkorMentah) does not satisfy Penilai (missing method Nilai), sehingga ketiadaan metode terdeteksi sebelum program dijalankan. Seluruh program pada topik ini dapat dijalankan lewat terminal sebagaimana berikut.

bash
# siapkan folder kerja untuk eksperimen topik ini
mkdir -p /tmp/generics/constraint
cp rerata-nilai.go rerata-suhu.go cari-elemen.go nilai-rerata.go /tmp/generics/constraint/
cd /tmp/generics/constraint

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

# jalankan keempat programnya
go run rerata-nilai.go
go run rerata-suhu.go
go run cari-elemen.go
go run nilai-rerata.go
text Keluaran
80
66.66666666666667
27.333333333333332
80
true
false
3
2
86
84.66666666666667

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul saat constraint baru ditulis sendiri.

  • Menuliskan union tanpa tanda tilde untuk tipe yang didefinisikan sendiri. Constraint int | float64 menolak []Suhu dengan pesan Suhu does not satisfy int | float64 (possibly missing ~ for float64 in int | float64). Perbaikannya, tulis ~int | ~float64 agar seluruh tipe dengan tipe dasar yang sama ikut tercakup.
  • Membandingkan nilai pada constraint any. Operator == ditolak dengan pesan invalid operation: v == target (incomparable types in type set). Perbaikannya, gunakan comparable bila fungsi memang hanya membutuhkan perbandingan kesamaan.
  • Memanggil fungsi dengan tipe yang tidak menyediakan metode yang dituntut constraint. Pesan T (type SkorMentah) does not satisfy Penilai (missing method Nilai) muncul saat kompilasi. Perbaikannya, lengkapi metodenya pada tipe tersebut atau longgarkan constraintnya.

Ringkasan#

  • Setiap constraint berupa interface, dan constraint any adalah interface kosong yang memuat semua tipe.
  • comparable menuntut dukungan operator == dan != serta menjadi syarat bagi tipe kunci peta.
  • Elemen union mencocokkan tipe secara persis, sedangkan tanda ~ memperluasnya ke semua tipe dengan tipe dasar yang sama.
  • Interface constraint kustom menuntut metode tertentu sehingga fungsi generik dapat memanggil perilaku tersebut dari setiap item.

Referensi#