İçeriğe geç
academia.sh

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 (comparableany 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.
  • ~T yalnız Tnin kendisini değil, temel tipi T olan 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat