Ders 11 / 13
Tip Kısıtları
Bir tip kısıtı da bir arayüzdür, ama yöntem değil tip kümesi listeler: aynı üç kısıt dört adayla sınandığında SadeceInt yalnız int'i, ~int içeren TemelTipiInt de temeli int olan adlandırılmış tipleri, ~int|~float64 birleşimi Sayisal da ikisini birden kabul ediyor — 1/4, 2/4, 3/4. Sınırlayıcı ölçüm, tip kümesi listeleyen bir arayüzün sıradan bir değer tipi olarak kullanılamadığını, yalnız yöntem listeleyen bir arayüzün hem kısıt hem değer tipi olabildiğini gösteriyor.
İçindekiler
Genel Fonksiyonlar dersi kısıtsız bir tip parametresinin gövdede neredeyse işe yaramaz
olduğunu ölçtü: any ile == de + de derlenmiyordu, yalnız dilimin kendi yapısına
bakan len gibi işlemler geçiyordu. O ders bu boşluğu dolduran mekanizmayı bir cümleyle
açık bırakmıştı: bir tip kısıtı, tip parametresinin kabul ettiği tiplerin kümesini ve o
kümenin garanti ettiği işlemleri kaynakta açıkça yazan bir arayüzdür. Bu ders o
cümleyi açıyor ve aynı soruyu tekrar soruyor: bu kısıt kaynakta nerede duruyor, ve neyi
kabul ettiğini nereden biliyoruz?
Yanıt kısa: kısıt bildirimde durur. Ortak ölçümde “tip kısıtının bildirimi” dört bildirim örneğinden biridir — gömme, derleme zamanı uyum kanıtı ve genel tipin örneklenmesiyle birlikte, kaynakta açıkça yazılan dört bağdan biri. Bu ders o bağın nasıl yazıldığını ve neyi sınırladığını ölçer: bir kısıt hangi tipleri kabul ediyor, bu kabulü genişletmenin bir bedeli var mı, ve kısıt arayüzünün kendisi başka nerelerde kullanılabiliyor.
Kısıt Bir Tip Kümesi Listeler
GN5. Sıradan bir arayüz bir yöntem kümesi listeler: interface{ Yaz() string }
diyen bir tip, Yaz adlı bir yöntemi olan her tipi kabul eder. Bir tip kısıtı da
sözdizimsel olarak bir arayüzdür — interface{ ... } yazılır — ama parantez içine yöntem
imzası yerine tip adları yazılabilir: interface{ int | float64 }, yalnız int ya da
yalnız float64 olan tipleri kabul eden bir tip kümesi tanımlar. Bu, aynı sözdiziminin
iki ayrı işte kullanılması: biri “bu yöntemleri sağla” diyor, öbürü “bu tiplerden biri ol”
diyor.
~ işareti bu kümeyi genişletir. ~int, yalnız intin kendisini değil, temel tipi
int olan her adlandırılmış tipi de kapsar — type Yas int yazıldığında Yas artık
~intin kümesine girer, ama intin kümesine girmez, çünkü Yas ile int aynı tip
değildir, yalnız aynı temel tipi paylaşırlar. Aşağıdaki ölçüm üç kısıtı dört adayla
sınıyor: intin kendisi, temeli int olan adlandırılmış bir tip (Yas), float64, ve
hiçbirine uymayan string.
// kisit.go — uc kisitin kac tipi kabul ettigi, derleyiciye sorularak sayiliyor
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
}
type kisitTanimi struct{ ad, govde string }
type tipTanimi struct{ ad, bildirim, deger string }
func main() {
kisitlar := []kisitTanimi{
{"SadeceInt", "interface{ int }"},
{"TemelTipiInt", "interface{ ~int }"},
{"Sayisal", "interface{ ~int | ~float64 }"},
}
tipler := []tipTanimi{
{"int", "", "3"},
{"Yas (temeli int)", "type Yas int\n", "Yas(3)"},
{"float64", "", "3.5"},
{"string", "", `"x"`},
}
fmt.Printf("%-16s", "kisit")
for _, t := range tipler {
fmt.Printf("%-20s", t.ad)
}
fmt.Println()
for _, k := range kisitlar {
fmt.Printf("%-16s", k.ad)
kabul := 0
for _, t := range tipler {
kaynak := fmt.Sprintf("package sinama\n%stype K %s\nfunc F[T K](a T) {}\nfunc G() { F(%s) }",
t.bildirim, k.govde, t.deger)
ok := derlenirMi(kaynak)
sonuc := "hayir"
if ok {
kabul++
sonuc = "evet"
}
fmt.Printf("%-20s", sonuc)
}
fmt.Printf("%d/4\n", kabul)
}
}
kisit int Yas (temeli int) float64 string SadeceInt evet hayir hayir hayir 1/4 TemelTipiInt evet evet hayir hayir 2/4 Sayisal evet evet evet hayir 3/4
GN6. Üç satır, aynı dört adayla sınandığında, kısıtın kaynaktaki yazımına göre üç ayrı
kabul sayısı üretiyor. SadeceInt yalnız intin kendisini kabul ediyor — Yas, temeli
int olsa bile ayrı bir tip olduğu için reddediliyor; bu, iki dizinin uzunluğu farklı
olduğunda ayrı tip sayılmasıyla aynı katılıktır (Diziler ve Dilimler dersi, M09/K01).
TemelTipiInt ~int yazdığı için Yası da kabul ediyor ve kabul sayısı ikiye çıkıyor.
Sayisal iki ~ teriminin birleşimini (|) listelediği için hem tam sayı hem ondalık
tarafını kapsıyor ve üçe çıkıyor — string ise üçünde de reddediliyor, çünkü hiçbir terim
onu kapsamıyor. Kabul sayısındaki bu artış kaynaktan doğrudan okunabilir: SadeceIntin
gövdesinde bir terim var, TemelTipiIntinkinde bir terim ama tilde’li, Sayisalınkinde
iki terim birleşimli. Kısıtın ne kadar geniş olduğu, kısıtın kendi bildirimindeki terim
sayısına ve tilde işaretine bakılarak, hiçbir çağrı yapılmadan önceden söylenebilir.
Bu ölçüm aynı zamanda bağın bozulduğunda nerede göründüğünü de gösteriyor: bir çağıran
kısıtın kümesinde olmayan bir tiple bir genel fonksiyonu örneklemeye çalıştığında, hata
çalışma zamanında değil derlemede çıkıyor — tablodaki her “hayir” bir derleme reddi,
string argümanıyla SadeceInt kısıtlı bir fonksiyonu çağırmaya çalışmanın bir panikle
değil bir derleme durmasıyla sonuçlanacağının kanıtı.
Kısıtın kendisi de örtük bir gerçekleştirim modeliyle çalışıyor: Yas tipi hiçbir yerde
“ben TemelTipiInti karşılıyorum” demiyor, type Yas int satırı bile bir kısıttan hiç
söz etmiyor. Bir tipin bir kısıtın kümesine girip girmediği, o tipin kendi kaynağına değil
temel tipine bakılarak, dışarıdan belirleniyor — Arayüz Kavramı dersinin ölçtüğü örtük
gerçekleştirim modeliyle aynı düzenek, burada yöntemler yerine tip kimliği üzerinden
işliyor. Fark şurada: bir arayüzü örtük karşılamak için bir tipin belirli yöntemleri
yazması gerekiyor, bir kısıtı örtük karşılamak için ise bir tipin hiçbir şey
yazmasına gerek yok — yalnız temel tipinin kısıtın listelediği terimlerden birine uyması
yeterli. Yas tipini tanımlayan satır, kısıt tanımlanmadan önce de yazılmış olabilirdi;
kısıt sonradan geldiğinde Yas otomatik olarak onun kümesine girdi, çünkü üyelik
Yasın kaynağına değil temel tipine bakıyor.
| işaretiyle ayrılan terimlerin oluşturduğu bu yapıya küme birleşimi denir — kısıtın
kabul ettiği tip kümesi, her terimin kendi kümesinin birleşimidir. Sayisalın kümesi
~intin kümesiyle ~float64ün kümesinin birleşimi; bir tip bu iki alt kümeden birine
girerse yeterli, ikisine birden girmesi gerekmiyor. Küme birleşimi terimleri istenildiği
kadar çoğaltılabilir (~int | ~int8 | ~int16 | ~int32 | ~int64), ama her terim kümeyi
yalnız genişletir, hiçbir terim var olan bir üyeliği geri almaz — bu yüzden yukarıdaki
tablo tek yönlü bir artış gösteriyor, 1’den 2’ye, 2’den 3’e; hiçbir kısıt bir öncekinden
daha az tip kabul etmiyor.
Kısıt genişledikçe verdiği garanti daralıyor: SadeceInt yalnız bir tipi kabul ediyor ama
o tipin bütün int işlemlerini (bit kaydırma dâhil) garanti ediyor; Sayisal üç tipi daha
kabul ediyor ama yalnız her iki ailenin de ortak desteklediği işlemleri (+, -, *, /,
karşılaştırma işleçleri) garanti ediyor — bit kaydırma float64 için tanımlı olmadığından
Sayisal kısıtlı bir gövdede yazılamaz. Kısıtın kaynaktaki genişliği ile gövdenin
yazabileceği işlemlerin darlığı ters orantılı; bu, Genel Fonksiyonlar dersinin any
kısıtıyla gösterdiği en uç durumun (hiçbir işlem yazılamaması) daha ölçülü bir hâli.
comparable: Kısıt Boşluğunu Kapatan Yerleşik Kısıt
Genel Fonksiyonlar dersinin son ölçümü, kısıtsız T ile a == b yazan bir kaynağın
derlenmediğini göstermişti. Nedeni artık kaynaktan okunabilir: any hiçbir tip kümesi
terimi taşımıyor, yalnızca “her tip kabul edilir” diyor ve == işlecinin var olduğuna
dair hiçbir söz vermiyor. Dil bu boşluğu kendi yerleşik kısıtıyla kapatıyor:
comparable, == ve != işleçlerini destekleyen her tipi kapsayan bir tip kümesi
tanımlar — dilim, sözlük ve fonksiyon tipleri hariç, çünkü bu üçü Go’da hiçbir zaman
== ile karşılaştırılamaz.
// kapali.go — comparable kisiti gecen dersin kisitsiz == hatasini kapatiyor
package main
import "fmt"
func Esit[T comparable](a, b T) bool { return a == b }
func main() {
fmt.Println("Esit(3, 3) :", Esit(3, 3))
fmt.Println("Esit(\"x\", \"y\") :", Esit("x", "y"))
}
Esit(3, 3) : true
Esit("x", "y") : false
GN7. Esit burada, önceki dersin Esit[T any] yazımıyla aynı gövdeye sahip — tek fark
kısıtın adı. Bu tek sözcük, == işlecinin gövdede kullanılabilmesi için gereken bilgiyi
taşıyor; comparable any gibi standart kitaplığın bir parçası değil, dilin kendi
sözdiziminin bir üyesidir, tıpkı anynin kendisi gibi. Bağın nerede kurulduğu burada
iki katmanlı: kısıtın hangi işlemlere izin verdiği kısıtın kendi bildiriminde durur
(comparable mı any mi seçildi), ama o izin verilen işlemin hangi somut tip için
çalışacağı yine çağrı yerinde, örnekleme sırasında kurulur.
Sınırlayıcı Ölçüm: Kısıt Arayüzü Yalnız Kısıt Konumunda Geçerli
Yukarıdaki üç kısıt da birer arayüz. Sıradan bir arayüz iki işte kullanılabilir: bir
değişkenin tipi olarak (var x Yazici) ve bir tip parametresinin kısıtı olarak
(func F[T Yazici](...)). Tip kümesi listeleyen bir kısıtın bu iki işten yalnız birini
yapabildiği, aşağıdaki ölçümün konusu.
// sinirlayici.go — kisit arayuzu deger tipi olarak kullanilamiyor, derleyiciye sorularak olculuyor
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() {
sayisalKisit := `package sinama
type Sayisal interface{ ~int | ~float64 }
func Topla[T Sayisal](a, b T) T { return a + b }`
sayisalDeger := `package sinama
type Sayisal interface{ ~int | ~float64 }
func F() { var x Sayisal; _ = x }`
yaziciKisit := `package sinama
type Yazici interface{ Yaz() string }
func F[T Yazici](a T) {}`
yaziciDeger := `package sinama
type Yazici interface{ Yaz() string }
func F() { var x Yazici; _ = x }`
fmt.Printf("%-38s %-12s %s\n", "arayuz", "kisit konumu", "deger tipi")
fmt.Printf("%-38s %-12s %s\n", "Sayisal (tip kumesi listeler)",
siniflaKaynak(sayisalKisit), siniflaKaynak(sayisalDeger))
fmt.Printf("%-38s %-12s %s\n", "Yazici (yontem listeler)",
siniflaKaynak(yaziciKisit), siniflaKaynak(yaziciDeger))
}
arayuz kisit konumu deger tipi Sayisal (tip kumesi listeler) derlendi derlenmedi Yazici (yontem listeler) derlendi derlendi
GN8. Sayisal, bir kısıt konumunda ([T Sayisal]) derleniyor ama sıradan bir
değişken tipi olarak (var x Sayisal) derlenmiyor: tip kümesi terimi taşıyan bir arayüz,
kaynağın söylediği kadarıyla, yalnız kısıt olarak var olabilir. Yazici ise ikisinde de
derleniyor — yöntem listeleyen bir arayüzün tip kümesi kısıtlaması yok, bu yüzden hem bir
değeri taşıyabilir hem bir kısıt olabilir. Arayüzün iki işi burada ayrışıyor: sözleşme
(hangi yöntemler var) ve kısıt (hangi tipler geçerli) aynı sözdizimle yazılır, ama
tip kümesi terimi eklendiği an arayüz artık yalnız ikinci işi yapabiliyor.
Bu ayrım Arayüz Kavramı dersinin kurduğu modeli bozmuyor, ona bir sınır ekliyor: bir
arayüzün örtük gerçekleştirimi hâlâ geçerli, ama bir arayüzün kendisinin nerede
kullanılabileceği artık arayüzün gövdesindeki terimlere bağlı. Sayisal bir tipin ne
yapabileceğini söylemiyor, hangi tipler olabileceğini söylüyor — ve bu, bir yöntem
sözleşmesinden farklı bir sorudur. Topla[T Sayisal] fonksiyonu Tnin + işlecini
desteklediğini biliyor, çünkü kısıtın tip kümesindeki her terim (~int, ~float64) bu
işleci destekliyor; ama bu bilgi bir yöntem çağrısından değil, kümenin üyeliğinden
geliyor.
Bu derste iki yer yan yana kondu ve fark her ikisinde de koşturularak gösterildi: kısıtın kendisi bildirimde durur (kaç terim, hangi tilde, hangi birleşim), ama kısıtın hangi somut tipe uygulanacağı yine kullanım yerinde, örnekleme sırasında kurulur. Genel Fonksiyonlar dersi bu ikinci yeri tek başına ölçmüştü; bu ders ona bildirim tarafını ekledi ve ikisinin birlikte nasıl çalıştığını gösterdi — kısıt olmadan kullanım yeri hiçbir işleme izin vermiyordu, kısıtla birlikte hangi işlemlerin izinli olduğu önceden, kaynaktan okunabilir hâle geliyor.
Bu iki işin aynı sözdizimle yazılması, kısıt kavramının Go’ya sonradan eklenmiş ayrı bir yapı olmamasının doğrudan sonucu: dil yeni bir anahtar sözcük açmak yerine mevcut arayüz sözdizimini genişletti ve tip terimlerini de kabul eder hâle getirdi. Bunun bedeli yukarıdaki ölçümde görüldü — genişleyen sözdizimin bir ucu (tip terimi eklemek) öbür ucu (değer tipi olarak kullanmak) kapatıyor. Bir kısıt yazan kişi bu yüzden iki soruyu birlikte düşünmek zorunda: kısıtın kabul edeceği tipler ne kadar geniş olmalı, ve bu kısıtın bir gün sıradan bir değer tipi olarak da kullanılması gerekip gerekmeyeceği. İkinci soru evetse, kısıt tip terimi taşımamalı — yalnız yöntem listelemeli; kısıt yalnız tip parametrelerini sınırlamak için varsa, tip terimi eklemenin bir bedeli yok, çünkü zaten başka bir işte kullanılması beklenmiyor.
Özet
- Bir tip kısıtı sözdizimsel olarak bir arayüzdür, ama yöntem yerine bir tip kümesi listeler; hangi tiplerin kabul edildiği kısıtın kendi bildiriminden okunur.
~TyalnızTnin kendisini değil, temel tipiTolan her adlandırılmış tipi de kapsar;|ile birleştirilen terimler kümeyi genişletir.- Aynı dört aday üç kısıtla sınandığında kabul sayısı kısıttaki terim sayısına ve tilde işaretine göre artıyor: 1/4, 2/4, 3/4.
- Kısıt bozulduğunda — bir çağıran kümede olmayan bir tiple örnekleme yaptığında — bağ çalışma zamanında değil derlemede görünür.
- Tip kümesi terimi taşıyan bir arayüz yalnız kısıt konumunda geçerlidir; yöntem listeleyen bir arayüz hem kısıt hem sıradan değer tipi olabilir.
- Arayüzün iki işi — sözleşme ve kısıt — aynı sözdizimle yazılır, ama tip kümesi terimi eklendiği an arayüz yalnız ikinci işe daralır.
Kısıt, kısıtsız anynin çizdiği en dar sınırı genişletiyor, ama sınırı tamamen
kaldırmıyor: bir kısıt ne kadar geniş yazılırsa yazılsın, gövde yine yalnız kısıtın
garanti ettiği işlemleri kullanabilir. Bu, tip güvenliğinin bedelinin nereye gittiğini
gösteriyor — güvenlik, gövdenin yazabileceklerini kısıtın bildiriminde önceden
sınırlamaktan geliyor.
Sonraki Adım
Kısıt, bir tip parametresinin kabul edildiği tipleri bildirimde sınırlıyor; ama kısıtın
kendisi, o tip parametresiyle kurulan bir veri yapısının nerede örnekleneceğini
söylemiyor. Sıradaki ders bunu ölçer: bir genel yapı Yigin[int] ve Yigin[string]
olarak iki kez örneklendiğinde, bu örneklemenin hangi tarafın bildiriminde — yapıyı yazan
tarafın mı, kullanan tarafın mı — durduğunu ve bu durmanın tip güvenliğine ne kazandırdığını.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.