İçeriğe geç
academia.sh

Ders 10 / 13

Genel Fonksiyonlar

Bir genel fonksiyonun tip parametresi kaynakta hiç yazılmıyor, ama derleyici onu çağrı yerinde çıkarımla kuruyor: aynı ilkOge kaynağı int, string ve float64 dilimleriyle üç kez çağrıldığında %T üç ayrı yanıt veriyor. Sınırlayıcı ölçüm, kısıtsız bir tip parametresinin gövdede neredeyse hiçbir işleme izin vermediğini gösteriyor: == ve + derlenmiyor, yalnız len gibi tipten bağımsız işlemler geçiyor.

İçindekiler

Panik ve Kurtarma dersi bir bağın hiçbir yerde kurulmadığını ölçtü: paniğin var olduğu bilgisi çağıranın imzasında görünmüyor, çağıran onu ancak koşum sırasında öğreniyordu. Bu konu tersini yapıyor. Genel bir fonksiyonun tip parametresi de kaynakta hiç yazılmaz — ilkOge[T any](d []T) çağrıldığında Tnin ne olacağı hiçbir çağrı satırında görünmez — ama bağ ortada kalmıyor: derleyici onu çağrı yerinde, argümanların tipine bakarak kurar ve kurduğu bağ hiçbir zaman değerin içine girmez. Bu, kursun sorduğu soruyu bir kez daha sorar: bu bağ nerede kuruluyor?

Ortak ölçümde on altı bağın dördü bildirimde, dördü kullanım yerinde, sekizi değerin içinde duruyor; bu dersin konusu olan tip parametresi çıkarımı, kullanım yerinde kurulan dört bağdan biridir. Genellikler ile arayüzler aynı soruyu iki ayrı yerde yanıtlıyor — arayüzde bağ değere girerken, genellikte hiç girmiyor — ve bu fark kursun üçüncü iddiasının konusu; bu ders o farkın ilk yarısını, kullanım yerini, kurar.

Genellikler öncesinde aynı işi iki yoldan biriyle yazmak gerekiyordu: ya her tip için ayrı bir fonksiyon (ilkOgeInt, ilkOgeDizgi, ilkOgeOndalik), ya da tek bir fonksiyon yazıp parametreyi boş arayüzle (any) almak ve gövdede tip iddiasıyla somut tipe geri dönmek. Birincisi kaynağı çoğaltıyor — aynı mantığın dört kopyası, dördü de bakım gerektiren ayrı bir yer. İkincisi kaynağı tek tutuyor ama tip bilgisini derleme zamanından çalışma zamanına itiyor: çağıran hangi tipi geçirirse geçirsin fonksiyon kabul ediyor, hatalı bir tip ancak koşum sırasında, bir tip iddiası başarısız olduğunda ortaya çıkıyor. Genel fonksiyon üçüncü bir yol açıyor: kaynak tek, ama tip denetimi derleme zamanında kalıyor — ilkOge bir []bool ile çağrılırsa bu satır derlenmeden önce reddedilir, T gövdede bir kez yazılır ve her çağrı için ayrıca kontrol edilir. Bu üç yazımın aynı işi nerede bağladığı — bildirimde çoğaltarak, değerde geciktirerek, kullanım yerinde derleyerek — kursun kapanış dersinin konusu; bu ders yalnız üçüncüsünü kurar.

Tip Parametresi Çağrı Yerinde Kuruluyor

Genel bir fonksiyon, adının hemen ardına köşeli parantezle bir tip parametresi listesi ekler: func ilkOge[T any](d []T) (T, bool). T burada somut bir tip değil, fonksiyon çağrıldığında doldurulacak bir yer tutucudur; any ise bu yer tutucunun kabul ettiği tiplerin sınırını söyler — şimdilik sınır yok, her tip geçerli. Fonksiyonun kendisi tek bir kaynakta yazılıdır ve bu kaynak, hangi somut tiple örnekleneceğinden bağımsızdır.

GN1. Fonksiyon gövdesindeki var sifir T satırı, Tnin ne olacağını hiç bilmeden yazılmış bir sıfır değer üretimidir; bu satır kaynakta bir kez durur ve her çağrıda aynen kalır — bir bildirimdir. Tnin kendisi ise her çağrıda değişir ve bu değişimin nerede olduğu kaynakta hiçbir yerde yazılı değildir; derleyici onu yalnız çağrı satırındaki argümanın tipine bakarak kurar — bir kullanım yeri bağıdır. Aynı fonksiyonun iki parçası, kursun iki ayrı sınıfına düşüyor.

// cikarim.go — tip parametresi cagri yerinde cikariliyor, sifir deger mantigi bildirimde yaziyor
package main

import "fmt"

func gozle(ad, yer, kanit string) {
	fmt.Printf("%-42s %-10s %s\n", ad, yer, kanit)
}

// ilkOge her tip icin ayni kaynaktan calisir; sifir T satiri fonksiyonun kendi
// bildiriminde durur ve hangi somut tipin gelecegini hic bilmez.
func ilkOge[T any](dilim []T) (T, bool) {
	var sifir T
	if len(dilim) == 0 {
		return sifir, false
	}
	return dilim[0], true
}

func sifirDeger[T any]() T {
	var s T
	return s
}

func main() {
	fmt.Println("-- bag nerede kuruluyor: genel fonksiyon --")

	i, _ := ilkOge([]int{7, 2, 9})
	gozle("tip parametresi (tamsayi dilimi)", "kullanim", fmt.Sprintf("cikarildi, T=%T", i))

	d, _ := ilkOge([]string{"elma", "armut"})
	gozle("tip parametresi (dizgi dilimi)", "kullanim", fmt.Sprintf("cikarildi, T=%T", d))

	o, _ := ilkOge([]float64{1.5, 2.5})
	gozle("tip parametresi (ondalik dilimi)", "kullanim", fmt.Sprintf("cikarildi, T=%T", o))

	gozle("sifir deger mantigi (var sifir T)", "bildirim", "ilkOge'nin kendi kaynaginda, hep ayni satir")

	bos := sifirDeger[string]()
	gozle("cikarimin yetmedigi cagri", "kullanim", fmt.Sprintf("sifirDeger[string]() aciktan yazildi, %T", bos))
}
-- bag nerede kuruluyor: genel fonksiyon --
tip parametresi (tamsayi dilimi)           kullanim   cikarildi, T=int
tip parametresi (dizgi dilimi)             kullanim   cikarildi, T=string
tip parametresi (ondalik dilimi)           kullanim   cikarildi, T=float64
sifir deger mantigi (var sifir T)          bildirim   ilkOge'nin kendi kaynaginda, hep ayni satir
cikarimin yetmedigi cagri                  kullanim   sifirDeger[string]() aciktan yazildi, string

Her çağrı, genel fonksiyonun bir örneklemesidir: ilkOge, T=int ile örneklendiğinde bir int dilimi üzerinde çalışan bir işleve, T=string ile örneklendiğinde bir dizgi dilimi üzerinde çalışan başka bir işleve karşılık gelir — ikisi de aynı kaynaktan, ama farklı bir tip bağlanmış olarak. Örnekleme sözcüğü bu dersten sonra iki kez daha karşımıza çıkacak: Tip Kısıtları dersinde bir kısıtın hangi örneklemeleri reddettiği, Genel Veri Yapıları dersinde bir örneklemenin kimin bildiriminde durduğu ölçülecek. Bu derste örnekleme her zaman çağıranın tarafında, argümanın statik tipine bakılarak oluyor — fonksiyonun kendisi hangi örneklemelerin isteneceğini bilmiyor.

var sifir T satırının üç ayrı örneklemede üç ayrı sonuç vermesi de aynı mekanizmadan geliyor. Sıfır Değerler dersi (M09/K01) her tipin kendi sıfır değerini tanımlı bulduğunu göstermişti — int için 0, string için boş dizgi, float64 için 0.0. var sifir T bu tanımı genel fonksiyonun içine taşıyor: kaynak hangi sıfır değerin üretileceğini hiç söylemiyor, yalnız “burada Tnin sıfır değeri dursun” diyor, ve derleyici örnekleme kurulduğunda bu sözü doldurur. Bu satırın kendisi bildirimde sabit kalırken, doldurduğu değer her örneklemede kullanım yerinden geliyor — aynı ayrımın küçük bir tekrarı.

GN2. ilkOge kaynağı üç çağrı boyunca tek harf değişmeden kalıyor; değişen yalnız argümanın taşıdığı dilimin öğe tipi. %T üç ayrı yanıt veriyor — int, string, float64 — ama bu üç yanıtın hiçbiri kaynakta aranarak bulunamaz, çünkü kaynakta Tnin yerini alacak bir ad hiç yazılmamış. Derleyici bu bağı, tıpkı Arayüz Kavramı dersinin örtük gerçekleştirimi gibi, kaynağın dışında kuruyor — ama arayüzden farklı olarak bu kararı çalışma zamanına bırakmıyor, derleme sırasında, argümanın statik tipine bakarak veriyor. Bu, iki bağın ortak noktası: ikisi de kullanım yerinde kurulur. Farkları da burada başlıyor: arayüz uyumu değere sorularak doğrulanabilir (%T ile), genel fonksiyonun tip parametresi ise derleme bittiğinde ortadan kalkar ve çalışan programda hiçbir yerde saklanmaz — bu ayrım Ne Zaman Genellik dersinde tekrar ölçülüyor.

Çıkarımın Yetmediği Yer

Derleyici tip parametresini yalnız argümanlardan okuyabiliyor; argüman yoksa okuyacak bir şey de yok. sifirDeger[T any]() T bunun en yalın örneği: fonksiyonun hiç parametresi yok, yalnız bir dönüş tipi var, ve dönüş tipinden geriye doğru çıkarım yapılamıyor. Yukarıdaki koşumda sifirDeger[string]() çağrısı bu yüzden Tyi köşeli parantezle açıkça taşıyor; bu satırdan [string]i silmek, “hangi tipin sıfır değeri” sorusunu derleyicinin yanıtlayamayacağı bir soruya çevirir ve kaynak derlenmez.

Çıkarımın yetmediği ikinci bir durum, argüman var olduğunda da ortaya çıkabilir: aynı tip parametresi birden çok parametrede geçiyorsa, derleyici hepsinin aynı somut tipe çözülmesini bekler. Aşağıdaki ölçüm bunu, kaynağı program içinden derleyiciye sorarak sınıyor.

// sinirlayici.go — cikarimin yetmedigi cagri ve kisitsiz govdenin sinirlari, derleyiciye sorularak olculur
package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

func derlenirMi(kaynak string) bool {
	dizin, err := os.MkdirTemp("", "sinama")
	if err != nil {
		return false
	}
	defer os.RemoveAll(dizin)
	os.WriteFile(filepath.Join(dizin, "go.mod"), []byte("module sinama\n\ngo 1.24\n"), 0o644)
	os.WriteFile(filepath.Join(dizin, "kod.go"), []byte(kaynak), 0o644)
	cmd := exec.Command("go", "build", "./...")
	cmd.Dir = dizin
	cmd.Env = append(os.Environ(), "GOTOOLCHAIN=local")
	return cmd.Run() == nil
}

func siniflaKaynak(kaynak string) string {
	if derlenirMi(kaynak) {
		return "derlendi"
	}
	return "derlenmedi"
}

func main() {
	cagriUyumsuz := `package sinama
func Ikisi[T any](a, b T) T { return a }
func F() { Ikisi(3, "x") }`

	cagriAcik := `package sinama
func Ikisi[T any](a, b T) T { return a }
func F() { Ikisi[any](3, "x") }`

	karsilastirma := `package sinama
func Esit[T any](a, b T) bool { return a == b }`

	toplama := `package sinama
func Topla[T any](a, b T) T { return a + b }`

	uzunluk := `package sinama
func Uzunluk[T any](d []T) int { return len(d) }`

	fmt.Printf("%-38s %s\n", "islem", "sonuc")
	fmt.Printf("%-38s %s\n", "cikarim: int ile string ayni T'ye", siniflaKaynak(cagriUyumsuz))
	fmt.Printf("%-38s %s\n", "aciktan T=any yazilinca", siniflaKaynak(cagriAcik))
	fmt.Printf("%-38s %s\n", "govdede kisitsiz T ile == ", siniflaKaynak(karsilastirma))
	fmt.Printf("%-38s %s\n", "govdede kisitsiz T ile +", siniflaKaynak(toplama))
	fmt.Printf("%-38s %s\n", "govdede kisitsiz T'nin dilimine len", siniflaKaynak(uzunluk))
}
islem                                  sonuc
cikarim: int ile string ayni T'ye      derlenmedi
aciktan T=any yazilinca                derlendi
govdede kisitsiz T ile ==              derlenmedi
govdede kisitsiz T ile +               derlenmedi
govdede kisitsiz T'nin dilimine len    derlendi

GN3. İlk iki satır aynı çağrının iki yazımı: Ikisi(3, "x") derlenmiyor çünkü derleyici Tyi birinci argümandan int, ikincisinden string olarak okumaya çalışıyor ve tek bir tipte uzlaşamıyor; Ikisi[any](3, "x") derleniyor çünkü tip parametresi artık çıkarıma bırakılmıyor, çağıran anyi kendisi yazıyor. Çıkarım bir kolaylıktır, bir zorunluluk değil — derleyici her zaman açık yazıma geri dönmeye izin verir, yalnız çıkarımın yapılamayacağı yerde açık yazım zorunlu hâle gelir.

Bu ret, iki parametrenin aynı tip parametresini paylaşmasından geliyor — func Ikisi[T any](a, b T) T imzası, a ile bnin aynı somut tipe çözülmesini bir bildirim olarak dayatıyor. İmza func Ikisi2[T1, T2 any](a T1, b T2) yazsaydı, a inte, b stringe ayrı ayrı bağlanır ve çağrı derlenirdi; iki değişkenin farklı tip olmasına izin verilmesi ya da verilmemesi, tip parametresi listesinin kaç ayrı ad taşıdığına bağlıdır. Bu da bağın konumunu bir kez daha doğruluyor: kaç tip parametresinin paylaşılacağı kaynakta yazılıdır (bildirim), o parametrelerin hangi somut tiplere karşılık geleceği ise her çağrıda yeniden kurulur (kullanım yeri).

Bu ayrım pratikte bir tasarım kararına dönüşüyor: iki parametrenin aynı tipte olması gerekiyorsa — örneğin bir karşılaştırma ya da bir takas işlevinde — tek bir tip parametresi paylaştırmak bu zorunluluğu derleyiciye devrediyor; çağıran yanlışlıkla iki ayrı tip geçirirse kaynak hiç derlenmiyor. İki parametrenin bağımsız olması gerekiyorsa ayrı tip parametreleri açmak, derleyicinin gereksiz yere bir eşitlik dayatmasını önlüyor. Kaynaktaki tip parametresi listesi böylece yalnız bir söz dizimi ayrıntısı değil, hangi argümanların birbirine bağlı olduğunu söyleyen küçük bir sözleşme.

Sınırlayıcı Ölçüm: Kısıtsız Gövdede Ne Yazılabilir

GN4. Son üç satır farklı bir soruyu ölçüyor: tip parametresi çağrı yerinde kurulduktan sonra, fonksiyonun gövdesinde o parametreyle ne yapılabilir? any yalnız “her tip kabul edilir” demek, “her işlem yazılabilir” demek değil. a == b derlenmiyor, çünkü Tnin kısıtı any, ve any karşılaştırılabilirliği garanti etmiyor — bir dilim ya da bir işlev de anyye uyar, ama ikisi de == ile karşılaştırılamaz. a + b da aynı nedenle derlenmiyor: + yalnız sayısal tiplerde ve dizgide tanımlı, any bunu garanti etmiyor. len(d) ise derleniyor, çünkü bu işlem Tnin kendisine değil dilimin tipine bakıyor — dnin uzunluğu, öğelerin tipinden bağımsız.

Bu üç satır kısıtsız bir tip parametresinin gövdede neredeyse işe yaramaz olduğunu gösteriyor: any en geniş kabulü sağlıyor ama en dar işlem kümesini bırakıyor. Bu sınırlama bir eksiklik değil, kursun ikinci iddiasının ters yüzü: bir tipin bir arayüzü örtük karşılaması kaynakta hiçbir yerde yazılmıyorsa, derleyicinin T hakkında hiçbir şey bilmediği de kaynakta hiçbir yerde yazılmıyor demektir; any bu bilgisizliği en uç noktasına taşıyor.

len(d) çizgisinin diğer ikisinden ayrı davranmasının nedeni, bu işlemin Tye değil dilimin kendi yapısına bakmasıdır: bir dilim, öğe tipi ne olursa olsun, aynı üç alanlı başlığı taşır ve uzunluk bu başlıktan okunur. index, range, cap gibi diğer yerleşik işlemler de aynı sınıftan — bunlar Tnin hangi tip olduğunu hiç sormaz, yalnız dilin kendisinin garanti ettiği yapıya bakar. == ve + ise tam tersi: ikisi de Tnin somut tipine özgü bir davranışı çağırıyor, ve kısıtsız bir T bu davranışın var olduğunu bile garanti etmiyor. Sıradaki ders bu boşluğu dolduran mekanizmayı — tip parametresine hangi işlemlerin izin verileceğini bildirimde söyleyen tip kısıtını — kurar; comparable adlı yerleşik kısıt, yukarıdaki Esit örneğini tek bir sözcükle derlenir hâle getirir.

Beş satırlık ölçümün tamamı derlenirMi adlı tek bir araçla üretildi: kaynağı geçici bir pakete yazıp derleyiciyi bir alt süreç olarak çağıran ve yalnız çıkış durumunu okuyan aynı yöntem, Go’nun Tasarım Amaçları dersinde de kullanılmıştı. Yöntem burada da aynı iki özelliği taşıyor — belirlenimci (aynı kaynak her koşumda aynı yanıtı verir, çünkü derleyicinin kendisi çağrılıyor) ve taşınabilir (yalnız standart kitaplığın süreç başlatma yeteneğine dayanıyor). Hata metninin hiç okunmaması bilinçli bir sınırdır: bu ders “neden reddedildi” sorusunu değil, “reddedildi mi” sorusunu ölçüyor, çünkü derleyicinin ürettiği hata metninin biçimi sürüme bağlı bir ayrıntıdır ve kursun ölçütü bu ayrıntıya dayanmıyor.

Özet

  • Genel bir fonksiyonun tip parametresi kaynakta hiçbir yerde yazılı değildir; derleyici onu çağrı yerinde, argümanların statik tipine bakarak kurar.
  • Fonksiyon gövdesindeki sıfır değer mantığı gibi tipten bağımsız kod bildirimde durur ve her çağrıda değişmez; tip parametresinin kendisi her çağrıda kullanım yerinde yeniden kurulur.
  • Çıkarım yalnız argümanlardan okunur: argüman yoksa (dönüş tipi tek başına) ya da argümanlar aynı tipe uzlaşmıyorsa, tip parametresinin çağrı yerinde açıkça yazılması gerekir.
  • Kısıtsız bir tip parametresi (any) gövdede yalnız her tipte geçerli işlemlere izin verir; == ve + gibi işlemler kısıtsız T ile derlenmez.
  • Bütün bu ölçümler derleyicinin kendisine sorularak yapıldı: hata metni okunmadı, yalnız bir kaynağın derlenip derlenmediği sınıflandı.

Bu sınır, genelliğin bir eksikliği değil, tanımının doğal bir sonucu: bir genel fonksiyon tek bir kaynaktan her T için çalışmak zorunda, ve bu yalnız Tnin somut tipinden bağımsız işlemlerle mümkün. Kısıt bu bağımsızlığı bozmadan hangi işlemlerin güvenle kullanılabileceğini genişletiyor.

Sonraki Adım

Kısıtsız T ile == ve + yazılamaması bir eksiklik gibi görünse de, aslında derleyicinin T hakkında bilmediği her şeyi olduğu gibi bırakmasının sonucu. Sıradaki ders bu bilgisizliği bir bildirimle kapatır: bir tip kısıtı, tip parametresinin kabul ettiği tiplerin kümesini ve o kümenin hangi işlemleri garanti ettiğini kaynakta açıkça yazan bir arayüzdür — ve bu arayüz, öteki arayüzlerden farklı bir işte kullanılır.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat