Ders 07 / 18
Fonksiyonlar
Adlandırılmış bir dönüş değerine hiç dokunulmadığında tipinin sıfır değerini taşıyor; bir dönüş değerinin hiç yazılmaması derleme zamanında reddediliyor, ama çağıranın döneni hiç yakalamadan yok sayması hiçbir şey demiyor — Go'da hata bir dönüş değeri olduğu için bu açıkta kalan davranış kursun en önemli örneği. Sınırlayıcı ölçüm: dönüş değeri değişkene alınıp kullanılmadığında bu açıklık kapanıyor ve kullanılmayan değişken reddi devreye giriyor.
İçindekiler
Önceki ders “else’siz erken dönüş”ü ölçtü ama işlevin döndürdüğü değerin kendisine hiç bakmadı. Bu ders o değere bakıyor: bir işlev birden fazla değer döndürdüğünde bunlardan biri yazılmazsa ne oluyor, adlandırılmış bir dönüş hiç dokunulmadan bırakıldığında hangi değeri taşıyor, ve çağıran taraf dönen değeri hiç yakalamazsa dil buna ne diyor?
Programlama Temelleri kursu (M01/K02) fonksiyon tanımını Python’la kurdu; orada bir işlev tek bir değer döndürüyordu ya da hiç döndürmüyordu, birden fazla değer bir demet olarak paketlenip tek bir dönüş yuvasından geçiyordu. Bu kavram burada tekrar kurulmaz — farkı tek cümleyle yazmak yeter: Go’da çoklu dönüş, imzanın kendisinin parçasıdır; bir demete paketlenmez, her değer kendi tipiyle ayrı ayrı yazılır. Bu ders, bu ayrı yuvaların yazılmadığında ne taşıdığını ölçüyor.
Çoklu Dönüş Değeri ve Adlandırılmış Dönüşün Sıfırı
Go’da bir işlev birden fazla değer döndürebiliyor ve bu değerler adlandırılabiliyor —
imzada yazılan adlar, işlevin gövdesinde sıradan birer değişken gibi kullanılabiliyor. bol
işlevi bunu gösteriyor: sonuc ve hata, func satırında adlandırılmış, gövdede hiç
return sonuc, hata yazılmadan, çıplak bir return ile dönüyor.
Sıfır değerler dersinde ölçülen adliDonus işlevi bu davranışı zaten bir satırda göstermişti:
func adliDonus() (sonuc int, hata error) { return }, hiçbir gövde çalıştırmadan iki sıfır
değeri dönüyordu. Bu ders aynı düzeneği, gövdesi gerçekten çalışan, koşula göre dallanan bir
işlevde ölçüyor — bol, iki farklı yoldan geçebiliyor ve her yolda adlandırılmış dönüşün ne
kadarına dokunulduğu değişiyor.
// fonksiyon.go — coklu donus degeri ve adlandirilmis donusun yazilmadiginda sifir donmesi
package main
import "fmt"
func bol(a, b int) (sonuc int, hata error) {
if b == 0 {
hata = fmt.Errorf("sifira bolme")
return
}
sonuc = a / b
return
}
func main() {
s1, h1 := bol(10, 2)
fmt.Printf("%-24s sonuc=%v hata=%v\n", "basarili bolme", s1, h1)
s2, h2 := bol(10, 0)
fmt.Printf("%-24s sonuc=%v hata=%v\n", "sifira bolme", s2, h2)
bol(10, 0)
fmt.Printf("%-24s %s\n", "yok sayilan cagri", "panik yok, program devam ediyor")
}
basarili bolme sonuc=5 hata=<nil> sifira bolme sonuc=0 hata=sifira bolme yok sayilan cagri panik yok, program devam ediyor
DT20. b == 0 dalında yalnız hataya değer atanıyor, sonuca hiç dokunulmuyor.
Yazdırılan sonuc=0, kaynakta hiçbir yerde sonuc = 0 yazılmadığı hâlde çıkıyor — intin
sıfır değeri, Sıfır Değerler dersinde ölçülen çerçevenin doğrudan bir uzantısı. Adlandırılmış
bir dönüş değeri, işlev başladığı anda kendi tipinin sıfır değeriyle önceden ilklendirilmiş
bir yerel değişken gibi davranıyor; gövde ona hiç dokunmazsa, döndürdüğü şey o ilk değerdir.
Bu ilklendirme, işlev çağrıldığı her seferinde tazeleniyor. bol(10, 2) ve bol(10, 0)
aynı işlevin iki ayrı çağrısı ve ikisinde de sonuc sıfırdan başlıyor; birinci çağrıda 5e
atanıyor, ikincisinde hiç atanmadığı için sıfırda kalıyor. Bir önceki çağrının sonucu,
sıradaki çağrıyı hiçbir şekilde etkilemiyor — her çağrı, adlandırılmış dönüşlerini kendi sıfır
değerleriyle taze başlatıyor.
İkinci satır bunu daha da netleştiriyor: hata atandığı için hata=sifira bolme görünüyor,
ama sonuc hâlâ 0. İki değer aynı return ifadesiyle dönüyor, ama yalnız biri kaynakta
elle yazıldı. Adlandırılmış dönüş, çoklu dönüş değerinin bazılarını yazıp geri kalanını
sıfır değerine bırakmanın yolunu açıyor — imzada iki ad birden görünse de, gövdenin ikisine de
dokunması zorunlu değil.
Çıplak return (return sonuc, hata yerine yalnız return) burada bir kısaltmadan fazlası.
Adlandırılmamış bir çoklu dönüşte her return ifadesi, dönecek değerleri açıkça sıralamak
zorundadır; adlandırıldığında, kaynakta iki kez tekrar edecek adları yazmadan, işlevin o anki
durumunu olduğu gibi dönmek mümkün oluyor. Bu, sonuc ve hatanın işlev boyunca paket
düzeyi değil yerel değişkenler gibi davrandığını gösteriyor — imzada bildirilmiş olsalar
da, gövdenin herhangi bir noktasında okunabilir ve değiştirilebilirler.
Adlandırmanın bir de belgeleme yanı var. func bol(a, b int) (int, error) yazılsaydı, imza
yalnız iki tipi bildirirdi; hangi değerin sonucu, hangisinin hatayı taşıdığını anlamak için
gövdeyi okumak gerekirdi. sonuc ve hata adlarının imzada yazılması, bu bilgiyi gövdeye
inmeden veriyor — imza kendi başına, hangi dönüş yuvasının neyi taşıdığını söylüyor. Bu
yüzden adlandırılmış dönüşler, kısa bildirimin tam tersi bir yönde çalışıyor: kısa bildirim
tip adını kaldırıyordu, adlandırılmış dönüş bir anlam adı ekliyor.
Bu belgeleme kazancı, adlandırmanın zorunlu olmamasıyla birlikte okunmalı. func bol(a, b int) (int, error) da geçerli bir imzadır ve tamamen aynı şekilde iki değer döndürür;
adlandırma yalnız okunabilirlik için tercih ediliyor, davranışı değiştirmiyor. Bir işlevin
adlandırılmış dönüşe ihtiyacı olup olmadığı, kaç değer döndürdüğüne değil, o değerlerin
anlamının imzadan ne kadar açık okunduğuna bağlı.
Yazılmayan Dönüş Değeri Reddediliyor, Kullanılmayan Değil
İlk derste ölçülen dört retten biri, dönüş değerinin yazılmamasıydı: func F() int { }
gövdesi hiçbir değer üretmeden bitiyordu ve derleyici bunu reddediyordu. Bu ders o reddin tam
tersini ölçüyor: dönüş değerini çağıran tarafın hiç kullanmaması aynı şekilde reddediliyor
mu? Sezgisel olarak “hayır” denebilir — bir işlevi çağırıp sonucunu kullanmamak, birçok
bağlamda tamamen sıradan bir işlem (bir günlük yazma çağrısının dönüşü genelde önemsizdir). Bu
ders sezgiyi değil, gerçek davranışı ölçüyor.
Üçüncü satır bu soruyu koşturarak yanıtlıyor. bol(10, 0) çağrısı, hiçbir değere atanmadan,
tek başına bir ifade olarak yazılıyor — iki dönüş değeri de (sonuç ve hata) sessizce
atılıyor. Program panik yapmıyor, derleyici hiçbir şey söylemiyor, çalıştırma normal şekilde
devam ediyor.
DT21. Bu satırda hiçbir değişken tanımlanmıyor — yalnız bir çağrı yapılıyor. İlk derste ölçülen “kullanılmayan değişken” reddi, tanımlanan bir adın hiç okunmamasına uygulanıyordu; burada tanımlanan hiçbir ad yok, o yüzden o kural devreye hiç girmiyor. Dönen iki değer, hiçbir yere yazılmadan, çağrının kendisiyle birlikte kayboluyor.
Bu, ilk derste ölçülen dört retle bir arada okunduğunda çarpıcı bir asimetri ortaya çıkarıyor.
func F() int { } gibi bir gövde, dönüş değerini yazmadığı için reddediliyordu — derleyici
imzanın verdiği sözü tutmayan bir gövdeyi kabul etmiyordu. Ama bol(10, 0) çağrısı, dönüş
değerini fazlasıyla üretiyor — işlev sözünü tutuyor, iki değer de hesaplanıyor — yalnız
çağıran taraf bu üretilmiş değerleri hiç talep etmiyor. Derleyicinin ilgisi üretimin
kendisiyle sınırlı; üretilen bir değerin sonradan ne olduğu, işlevin sınırları dışında kalan
bir mesele.
Bu, kursun açıkta sınıfının şimdiye kadarki en önemli örneği, çünkü Go’da hata bir
sıradan dönüş değeridir — ayrı bir mekanizma, bir istisna türü ya da zorunlu bir
yakalama yapısı yok. Bir işlev bir error döndürüyorsa, bu değeri okuyup okumamak tamamen
çağıran tarafın kararı. Dil, dönen bir hatanın kontrol edilmesini hiçbir şekilde
zorunlu kılmıyor; bol(10, 0)ın döndürdüğü hata, tıpkı bir günlüğe hiç yazılmamış bir olay
gibi sessizce yok oluyor.
Bu davranışın kaynakta hiçbir izi de kalmıyor. Üçüncü satırın çıktısı (“panik yok, program
devam ediyor”) bunu doğruluyor: sifira bolme hatası gerçekten üretiliyor, ama hiçbir yere
yazılmadığı için hiçbir etkisi olmuyor — ne bir panik, ne bir günlük satırı, ne bir çıkış
kodu değişikliği. Kaynağı okuyan biri, bol(10, 0) satırına bakarak bu çağrının bir hata
üretip üretmediğini bile göremez; bunu anlamak için işlevin tanımına bakmak gerekiyor.
Bu, açıkta kalan davranışın ortak özelliği: sonucu görmek için çağrı satırı yetmiyor,
tanımın kendisi okunmak zorunda.
Sınırlayıcı Ölçüm: Yok Sayılan Her Zaman Açıkta Kalmaz
Bu açıklığın bir sınırı var. Dönen değerleri hiç yakalamadan yok saymak serbestken, onları
bir değişkene alıp sonra kullanmamak serbest değil. Bunu ölçmek için ilk dersteki
derleyici çağrısına dönüyoruz ve bolu iki farklı kaynak parçasında çağırıyoruz — biri
sonuçları hiç yakalamıyor, öbürü yakalayıp yalnızca birini kullanıyor.
// derleyici.go — derleyiciyi program icinden cagiran yardimci
package main
import (
"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 sinif(ok bool) string {
if ok {
return "derlendi"
}
return "derlenmedi"
}
// sinama.go — tumuyle yok saymak ile degiskene alip yari kullanmamak ayni sey degil
package main
import "fmt"
func main() {
tumuyleYokSayilan := `package sinama
import "fmt"
func bol(a, b int) (sonuc int, hata error) {
if b == 0 {
hata = fmt.Errorf("sifira bolme")
return
}
sonuc = a / b
return
}
func F() { bol(10, 2) }`
yariYokSayilan := `package sinama
import "fmt"
func bol(a, b int) (sonuc int, hata error) {
if b == 0 {
hata = fmt.Errorf("sifira bolme")
return
}
sonuc = a / b
return
}
func F() { s, h := bol(10, 2); _ = s }`
fmt.Printf("%-34s %s\n", "tumuyle yok sayilan", sinif(derlenirMi(tumuyleYokSayilan)))
fmt.Printf("%-34s %s\n", "degiskene alinip yari kullanilan", sinif(derlenirMi(yariYokSayilan)))
}
tumuyle yok sayilan derlendi degiskene alinip yari kullanilan derlenmedi
DT22. İki kaynak da aynı bol işlevini çağırıyor ve aynı sonucu (hata) yok sayıyor;
aradaki tek fark, ikinci kaynağın dönen değerleri s ve h adlı değişkenlere alması.
Birinci kaynakta hiçbir ad tanımlanmadığı için kullanılmayan değişken reddi hiç devreye
girmiyor. İkinci kaynakta h adı tanımlanıyor ve hiç okunmuyor — _ = s yalnız syi
kurtarıyor, h kullanılmayan değişken kalıyor ve derleme reddediliyor.
Bu, “açıkta” sınıfının sınırını netleştiriyor. Bir dönüş değerini yakalamamak, dilin hiçbir zaman göremeyeceği bir karardır — çağrı satırında hiçbir ad yok, denetlenecek bir şey yok. Ama bir kez bir ada bağlandığında, o ad artık ilk derste ölçülen kuralın kapsamına giriyor: yazılan her ad ya kullanılmalı ya da kaynakta hiç yazılmamalı. Hatanın kendisi “açıkta” kalmaya devam ediyor — dil hâlâ onu okumanızı zorunlu kılmıyor — ama onu bir ada bağlayıp sonra unutmak, farklı bir kurala takılıyor.
Bu ayrım, Go’da hata denetiminin neden çoğunlukla sonuc, hata := bol(...) biçiminde
yazıldığını da açıklıyor. hata bir değişkene alındığı anda, onu hiç okumadan bırakmak
derlemeyi durduruyor — if hata != nil yazmak bir stil tercihi değil, hata adını
kullanılmış saymanın en doğal yolu. Dönüş değerini yakalamadan tamamen görmezden gelmek
hâlâ mümkün, ama bir kez yakalandığında görmezden gelmek artık mümkün değil.
Bu, kaynağı okuyan biri için de bir işaret taşıyor: bir çağrının sonucu hiç yakalanmadıysa,
o çağrının hatasının hiç denetlenmediği kesin — ama bir sonuç yakalandıysa, o sonucun en
azından derlenebilir olacak kadar kullanıldığı da kesin. Kullanım, denetim anlamına
gelmiyor — _ = h yazıp hyi hiç okumadan da “kullanılmış” sayılabilirdi — ama en azından
adın kaynakta bir amaçla durduğunu gösteriyor. Bu, ilk derste ölçülen boş tanımlayıcının
üçüncü bir kullanımı: _ = h yazmak, “bu hatayı biliyorum ama bilerek denetlemiyorum” demenin
yazılı biçimi. Kullanılmayan değişken reddi, bir hatayı denetlemeye zorlamıyor — yalnız
onu isimlendirip unutmaya izin vermiyor.
Aradaki fark küçük görünse de pratikte önemli: bir kod tabanında hangi hataların hiç
yakalanmadığını bulmak için çağrı satırlarını taramak gerekir (derleyici burada hiçbir iz
bırakmaz), ama hangi yakalanmış hataların denetlenmediğini bulmak biraz daha kolaydır — en
azından _ = hata gibi bilinçli bir görmezden gelme izini aramak mümkündür. Derleyici birinci
grubu hiç göremiyor, ikinci grubu da yalnız “kullanılmadı mı” sorusuyla görüyor, “denetlendi
mi” sorusuyla değil.
Bu dersin üç ölçümü — adlandırılmış dönüşün sıfırı, yazılmayan dönüşün reddi, kullanılmayan
dönüşün açıklığı — kursun üç sınıfının üçünü de tek bir işlev üzerinde gösteriyor. bolun
kendisi hiç değişmedi; değişen, onu nasıl çağırdığımız. Aynı işlev, adlandırılmış bir
dönüşle çağrıldığında tanımlı bir sıfır değer taşıyor, imzasız bırakıldığında derlemeyi
durduruyor, çağrıda yakalanmadan bırakıldığında sessizce kayboluyor. Yazılmayanın karşılığı,
bu derste ilk kez işlevin kendisine değil, işlevin çağrılma biçimine bağlı hâle
geliyor.
Üç ölçümü bir arada okuduğumuzda, Go’nun fonksiyon tasarımının aynı ilkeyi üç kere tekrarladığı görülüyor: üretim zorunlu, tüketim değil. Bir işlev, imzasının söz verdiği her değeri üretmek zorunda — ya elle yazarak ya da adlandırılmış dönüşün sıfırına güvenerek. Ama o üretilmiş değeri çağıran tarafın alması, dilin hiçbir zaman zorunlu kıldığı bir şey değil. Yalnız bir kez alındığında, artık o alınan şeyin de kaynakta bir amaca hizmet etmesi gerekiyor.
Özet
- Adlandırılmış bir dönüş değerine gövde hiç dokunmazsa, o değer kendi tipinin sıfır değerini
taşır —
sonuc=0, kaynakta hiçbir yerdesonuc = 0yazılmadan çıkıyor. - Bir dönüş değerinin hiç yazılmaması (
func F() int { }) derleme zamanında reddediliyor. - Çağıranın dönen değerleri hiç yakalamadan yok sayması hiçbir şekilde reddedilmiyor — Go’da hata bir dönüş değeri olduğu için bu, kursun en önemli açıkta örneği.
- Dönen değerler bir değişkene alındığında durum değişiyor: kullanılmayan değişken reddi devreye giriyor ve yarım kullanım artık derlenmiyor.
- Hatanın kendisi hep açıkta kalıyor; onu bir ada bağlayıp sonra unutmak farklı, ayrı bir kurala takılıyor.
- Fonksiyon tasarımının ilkesi tek: bir işlev söz verdiği her değeri üretmek zorunda, ama çağıran tarafın o değeri alması dilin hiçbir zaman zorunlu kıldığı bir şey değil.
Sonraki Adım
Bu derste bol işlevi hep iki sabit argümanla çağrıldı ve dönüş tarafındaki yazılmamışlıklar
ölçüldü — argüman tarafında hiçbir belirsizlik yoktu, imza kaç argüman istediğini kesin olarak
söylüyordu. Ama bazı işlevler kaç argüman alacağını önceden bilmiyor — çağıran taraf sıfır, bir
ya da yirmi değer geçebiliyor. Sıradaki ders, A payının son dersi, bu belirsizliği ölçüyor:
değişken sayıda argüman alan bir parametre, hiç argüman verilmediğinde ne oluyor, ve bu
dersin kurduğu “yakalanan değerin adı” ile bir dilimin kendisi arasında nasıl bir ilişki var?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.