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ızTile 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.