İçeriğe geç
academia.sh

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 yerde sonuc = 0 yazı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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat