Ders 05 / 12
Fonksiyonlar ve İfade Odaklılık
Bir fonksiyonun gövdesi bir bloktur ve blok zaten bir ifadedir: son satır noktalı virgülsüzse gövdenin — dolayısıyla fonksiyonun — dönüş değeri odur, noktalı virgül eklenince gövde () döndürür; noktalı virgül değeri atar ama atılan değerin hesaplanması yine de gerçekleşir ve bu, atılan değerin kendi yan etkisiyle gösterilir.
Yayın Güncelleme
Bu derste
- Bir gövdenin son satırının noktalı virgülle bitip bitmemesinin dönüş değerini nasıl belirlediğini koşturarak gösterir
- Noktalı virgülün bir değeri sessizce attığını, ama atılan değerin hesaplanmasının yine de gerçekleştiğini bir yan etkiyle kanıtlar
- return yazımının yalnız gövdenin doğal sonundan önce çıkmak için gerektiğini, tail ifadede gerekmediğini karşılaştırarak ayırt eder
İçindekiler
Önceki dört ders sırasıyla üç sınıfı, kaynağın zorunlu iskeletini, bağlamayı ve temel tipleri kurdu. Bu ders hepsini tek bir noktada birleştiriyor: bir fonksiyonun gövdesi bir bloktur ve blok zaten Rust’ın Tasarım Hedefleri dersinde bir ifade olarak tanımlanmıştı. Bir fonksiyonun dönüş değeri, dolayısıyla, ayrı bir mekanizma değildir — gövde bloğunun kendi değeridir. Bu ders kursun eksenini en açık biçimde taşıyor: noktalı virgülün varlığı ya da yokluğu, bir fonksiyonun ne döndürdüğünü tek başına belirliyor.
Bu, “fonksiyon” kavramının Rust’ta neden ayrı bir dönüş sözdizimine ihtiyaç duymadığını açıklıyor. Birçok yazımda bir fonksiyonun dönüş değeri, gövdenin geri kalanından ayrı, kendi başına bir yapı gibi ele alınır. Rust bunun yerine daha önce kurulmuş bir kuralı yeniden kullanıyor: bir fonksiyon çağrıldığında yapılan şey, gövdesini bir blok olarak değerlendirmektir ve blok değerlendirmesinin sonucu ne ise fonksiyonun sonucu da odur. Yeni bir kavram öğrenmek gerekmiyor — önceki derslerde kurulan “blok bir ifadedir” kuralı, burada bir fonksiyon çağrısının tamamını açıklamaya yetiyor.
Gövdenin Son Satırı Dönüş Değeridir
Bir blok, önceki derslerde görüldüğü gibi, son satırı noktalı virgülsüzse o satırın
değerini verir; noktalı virgülle bitiyorsa ya da boşsa () verir. Bir fonksiyonun gövdesi
de bir blok olduğu için aynı kural onun için de geçerli: gövdenin son ifadesi noktalı
virgül almazsa, fonksiyonun kendisi o değeri döndürür.
Bu kural gövdenin her satırı için değil, yalnız son satırı için geçerli. Bir
fonksiyon gövdesindeki ara satırların hepsi normal deyimlerdir ve genellikle noktalı
virgülle biter — let bağlamaları, yan etkili çağrılar, döngüler. Yalnız gövdenin en son
satırı özel bir konumdadır: orası tail konumudur ve bloğun kendisinin (dolayısıyla
fonksiyonun) değerini belirleyen tek yer. Aşağıdaki örnek bu tek satırlık kuralın etkisini
iki farklı gövde üzerinden gösteriyor.
// fonksiyon_govdesi.rs — govdenin son satiri donus degerini belirler
fn hesapla_ve_izle(x: i32) -> i32 {
println!("hesaplaniyor: {} icin", x);
x * 2
}
fn ifade_olarak(x: i32) -> i32 {
hesapla_ve_izle(x)
}
fn deyim_olarak(x: i32) {
hesapla_ve_izle(x);
}
fn main() {
let sonuc = ifade_olarak(21);
println!("ifade_olarak donus degeri: {}", sonuc);
let deyim_sonucu = deyim_olarak(21);
println!("deyim_olarak donus degeri: {:?}", deyim_sonucu);
}
hesaplaniyor: 21 icin ifade_olarak donus degeri: 42 hesaplaniyor: 21 icin deyim_olarak donus degeri: ()
ifade_olarak ile deyim_olarak aynı çağrıyı sarıyor — ikisi de hesapla_ve_izle(x)i
çağırıyor — ama gövdelerinin son satırı farklı yazıldı. ifade_olarakta çağrı noktalı
virgülsüz, gövdenin tail ifadesi; deyim_olarakta aynı çağrının sonuna bir noktalı virgül
eklendi. Bu tek karakterlik fark, iki fonksiyonun imzasını da değiştirmek zorunda
bıraktı: ifade_olarakın dönüş tipi -> i32 olarak yazılabilirken, deyim_olarakın dönüş
tipi hiç yazılmadı, çünkü varsayılan dönüş tipi ()tir ve gövde artık () üretiyor. Bu iki
imza birbirinin yerine geçemez — deyim_olaraka -> i32 yazılsaydı, gövdesi () ürettiği
için derleyici reddederdi.
RD13. Bu, önceki dersteki birim: () örneğiyle aynı olguyu, fonksiyon sınırında
gösteriyor. Bir fonksiyonun dönüş tipi, gövdenin ürettiği değerin tipiyle birebir
eşleşmek zorunda; gövde i32 üretiyorsa imza -> i32 ya da uyumlu bir tip yazmalı, gövde
() üretiyorsa imza ya () yazmalı ya da hiç yazmamalı. Bu eşleşme derleme zamanında
denetleniyor — gövde ile imza arasında bir uyumsuzluk, çalışma zamanına hiç ulaşmadan
durduruluyor.
Bu denetimin somut bir sonucu var: deyim_olarakın gövdesine dokunmadan yalnız imzasını
-> i32 olarak değiştirmek yeterli değildir, çünkü gövde hâlâ () üretiyor. Aynı şekilde,
ifade_olarakın gövdesindeki tail çağrısına bir noktalı virgül eklemek de imzayı bozar —
gövde artık () üretirken imza hâlâ -> i32 yazıyor olurdu. Bu iki durumun ikisi de
derleyici tarafından reddedilir; kaynak bloğuna konmuyor çünkü ikisi de bir ret örneğidir,
ama sonucu şudur: gövde ile imza arasındaki bu bağ, kaynağın herhangi bir yerinde tek taraflı
değiştirilemez. Noktalı virgülü eklemek ya da çıkarmak, yalnız gövdenin son satırını değil,
fonksiyonun dış dünyaya verdiği sözü de değiştiriyor.
Sınırlayıcı Ölçüm: Noktalı Virgül Bir Değeri Sessizce Atar
Çıktının ilk iki satırı da dikkat çekiyor: hesaplaniyor: 21 icin mesajı iki kez
basıldı — bir kez ifade_olarak çağrısında, bir kez deyim_olarak çağrısında. İkinci
çağrının sonucu (42) hiçbir yerde kullanılmadı, deyim_sonucu yalnız () tuttu. Ama
hesapla_ve_izlenin içindeki yazdırma satırı yine de çalıştı, çünkü fonksiyonun gövdesi
her iki çağrıda da tam olarak koşturuldu; değişen yalnız sonucun ne olduğu değil, sonucun
nereye gittiğiydi.
Bu, noktalı virgülün ne yaptığını netleştiriyor: noktalı virgül bir hesaplamayı iptal
etmiyor, yalnız o hesaplamanın sonucunu atıyor. hesapla_ve_izle(x); yazımı, önce
hesapla_ve_izle(x)i tıpkı noktalı virgülsüz hâliyle aynı biçimde çalıştırıyor — içindeki
her satır koşuyor, içindeki her yan etki (burada yazdırma) gerçekleşiyor — sonra üretilen
42 değerini hiçbir adla saklamadan bırakıyor. Bu atma işlemi sessizdir: derleyici bir
uyarı basmaz, çalışma zamanı bir iz bırakmaz; atıldığını gösterecek tek şey, değerin
kendisinin bir sonraki satırda hiçbir yerde görünmemesidir. Bu derste bu sessizliği görünür
kılmanın tek yolu, hesaplamanın içine bağımsız bir yan etki (yazdırma) yerleştirmekti —
yan etki olmasaydı, deyim_olarakın içinde gerçekten bir çarpma işleminin çalışıp
çalışmadığını dışarıdan ayırt edemezdik.
Bu bulgunun pratik bir sonucu var: bir fonksiyonun dönüş değerini görmezden gelmek, o
fonksiyonun içindeki işi görmezden gelmek anlamına gelmiyor. deyim_olarak(21) çağrısı,
sonucu hiç okunmasa bile, hesapla_ve_izlenin bütün gövdesini çalıştırdı. Bir fonksiyonun
yalnızca bir yan etki için mi yoksa yalnızca bir değer için mi çağrıldığı, kaynakta çağrının
sonuna noktalı virgül konup konmamasından anlaşılıyor — ikisi de aynı işi yapıyor, farklı
olan yalnız sonucun okunup okunmadığı.
Bu gözlem, bir yazdırma makrosu çağrısının neden hem bir yan etki hem de bir dönüş değeri
taşıyabildiğini de açıklıyor: çağrı ekrana bir satır basıyor (yan etki) ve aynı zamanda
() değerini üretiyor (dönüş değeri). İki tarafın ayrı ayrı var olması, birinin
kullanılmasının öbürünü iptal etmediğini gösteriyor — bir çağrının sonucu okunmasa bile, o
çağrının içindeki her satır çalışır.
Bu ayrım, önceki dersteki Vec::push ile HashSet::insert karşılaştırmasının tersinden bir
görünümüdür. Orada aynı iş (ekleme) iki ayrı yönteme iki ayrı sınıf veriyordu, çünkü
yöntemlerin imzaları farklıydı. Burada ise aynı çağrı (hesapla_ve_izle(x)), çağıran
tarafın onu nasıl yazdığına bağlı olarak iki ayrı sınıfa girebiliyor: noktalı virgülsüz
yazılırsa ifade olarak kullanılıyor, noktalı virgüllü yazılırsa deyim olarak kullanılıyor.
Sınıf orada yöntemin kendi imzasından geliyordu; burada çağıranın tercihinden geliyor —
ama her iki durumda da gövdenin içindeki iş, sınıftan bağımsız olarak aynı kalıyor.
return’ün Gerektiği Yer
Gövdenin son satırı dönüş değerini belirlediğine göre, return anahtar sözcüğü ne zaman
gerekiyor? Yanıt: yalnız gövdenin doğal sonundan önce çıkmak istendiğinde. Bir
fonksiyon, koşullu bir dalın içinde erken bir sonuca ulaşıp gövdenin geri kalanını hiç
çalıştırmak istemiyorsa, o erken çıkışı return ile yazmak zorunda — çünkü o dal gövdenin
tail konumunda değil, ortasında.
Bu, returnün bir zorunluluk değil bir seçim olduğunu gösteriyor. Önceki bölümdeki her
iki fonksiyon da geçerliydi ve ikisi de derlendi; aralarındaki fark, gövdenin dallanmayı
nasıl düzenlediğiydi. Bir gövde, bütün olası sonuçları tek bir büyük ifadenin dalları olarak
düzenleyebiliyorsa, o ifade tail konumuna yerleştirilip hiç return yazılmadan aynı sonuca
ulaşılabiliyor. Bir gövde, bazı durumlarda geri kalan mantığı hiç çalıştırmadan erken
çıkmak istiyorsa, o çıkış tail konumunda olamaz — çünkü tail konumu tanım gereği
gövdenin en sonudur ve erken çıkış tam olarak “sona kadar gitme” demektir.
// erken_donus.rs — return yalniz govdenin dogal sonundan once cikmak icin gerekir
fn isaretle_donusle(x: i32) -> i32 {
if x < 0 {
return -1;
}
if x == 0 {
return 0;
}
1
}
fn isaretle_ifadeyle(x: i32) -> i32 {
if x < 0 {
-1
} else if x == 0 {
0
} else {
1
}
}
fn main() {
for deger in [-5, 0, 5] {
println!("{}: donusle={} ifadeyle={}", deger, isaretle_donusle(deger), isaretle_ifadeyle(deger));
}
}
-5: donusle=-1 ifadeyle=-1 0: donusle=0 ifadeyle=0 5: donusle=1 ifadeyle=1
İki fonksiyon da aynı üç sonucu üretiyor, ama yalnız isaretle_donusle return kullanıyor.
isaretle_donuslede her if bloğu gövdenin ortasında duruyor — gövde onlardan sonra
1 satırıyla devam ediyor — bu yüzden erken çıkmak isteyen bir dal, kendi değerini
return ile açıkça bildirmek zorunda; aksi hâlde o if bloğunun kendisi bir ifade olarak
değerlendirilir ama sonucu hiçbir yere bağlanmaz ve gövde kendi doğal sonuna kadar devam
eder. Burada dikkat edilmesi gereken bir nokta var: if x < 0 { return -1; } yazımındaki
if bloğunun kendisi hâlâ () üretiyor, çünkü return bloğun içinden fonksiyonun
tamamına atlıyor, bloğun kendisine bir değer bırakmıyor. returnün etkisi blok sınırını
aşıyor; noktalı virgül ve tail konumu kuralları yalnız o bloğun kendi değerini
belirlerken, return doğrudan en dıştaki fonksiyon çağrısına dönüyor. isaretle_ifadeylede ise bütün dallanma tek bir if/else if/else zincirine
toplandı ve bu zincirin kendisi gövdenin tail ifadesi oldu; hiçbir dal erken çıkmıyor,
üçü de aynı büyük ifadenin bir parçası olarak değerlendiriliyor ve zincirin seçtiği dal
doğrudan fonksiyonun dönüş değeri oluyor.
RD14. return, bir ifadenin değerini değiştirmiyor; bir ifadenin ne zaman
değerlendirileceğini, daha doğrusu gövdenin geri kalanının hiç değerlendirilmeyeceğini
belirliyor. isaretle_donuslede x < 0 doğruysa, return -1;den sonraki hiçbir satır
çalışmaz — ikinci if bloğu bile değerlendirilmez. isaretle_ifadeylede ise üç dal da aynı
zincirin parçası olduğu için, yalnız biri seçiliyor ama “erken çıkma” diye bir kavram yok;
seçilmeyen dallar hiç değerlendirilmiyor olsa da, bu normal dallanma davranışı, erken çıkış
değil.
Çıktı bu farkı doğrudan göstermiyor — üç değer için de iki fonksiyon aynı sonucu veriyor,
çünkü ikisi de aynı mantığı farklı bir düzenlemeyle koduyor. Farkın nerede yattığını görmek
için koşulan satırların sayısına değil, kaynaktaki kontrol akışının şekline bakmak
gerekiyor: isaretle_donuslede negatif bir girdi için yalnız bir if gövdesi çalışıyor ve
fonksiyon oradan hemen çıkıyor; isaretle_ifadeylede aynı girdi için üç dalın hepsi aynı
zincirin parçası olarak değerlendiriliyor, yalnız biri seçiliyor. İki yaklaşım da doğru
sonuca varıyor, ama birinde “erken çıkış” diye adlandırılabilecek bir an var, öbüründe hiç
yok — orada yalnız bir seçim var.
Bu iki yazım arasındaki tercih bir okunabilirlik sorusudur, doğruluk sorusu değil: ikisi de
aynı üç sonucu üretiyor ve ikisi de eşit derecede geçerli. Ama gövde uzadıkça ve erken
çıkılması gereken özel durum sayısı arttıkça, return ile yazılan biçim genellikle özel
durumları tail ifadenin dışına, kendi kontrol noktalarına taşıyarak gövdenin geri kalanını
sadeleştiriyor; tek bir dallanmayla ifade edilebilecek kadar basit gövdeler ise return
yazmadan, tek bir tail ifadeyle de aynı sonuca ulaşabiliyor.
isaretle_donuslenin yapısı bunu somutlaştırıyor: iki özel durum (x < 0, x == 0) kendi
kontrol satırlarında ele alınıp gövdenin geri kalanından ayrıştırılmış, geriye kalan tek
genel durum (1) ise gövdenin doğal sonunda, hiçbir koşula bağlı olmadan duruyor. Okuyan
taraf ilk iki ifi “özel durum, buradan çık” olarak, son satırı da “artık geriye kalan tek
olasılık bu” olarak okuyabiliyor. isaretle_ifadeylede ise üç durum da eşit ağırlıkta,
tek bir zincirin parçası olarak sunuluyor — hiçbiri diğerinden “daha erken” ya da “daha
istisnai” değil. İki yazım da doğru, ama okuyana verdikleri sinyal farklı: biri “çoğu
durum geride kalan tek satırdır, öncesindekiler sapmadır” diyor, öbürü “üç durum da eşit
önemde” diyor.
return yazılan fonksiyonlarda bir düzen daha var: gövdenin doğal sonu, genellikle en
genel ya da en beklenen durumu taşır, çünkü oraya ulaşmak, önceki bütün özel
kontrolleri geçmiş olmak demektir. Bu, kaynağı yukarıdan aşağı okuyan biri için bir tür
sıralama sağlıyor: en çok karşılaşılan yol en aza indirgenmiş, özel durumlar öne alınıp
elenmiş oluyor. Bu sıralama Rust’ın zorunlu kıldığı bir kural değil, returnün doğal
kullanım biçiminden doğan bir alışkanlık.
Bu ders, kursun ilk dersinde kurulan üç sınıfın (ifade, deyim, öğe) neden yalnız tek tek
satırlar için değil, bütün bir fonksiyon gövdesi için de geçerli bir çerçeve olduğunu
gösterdi. Bir fn bildirimi öğedir ve kendisi değer üretmez; ama o öğenin gövdesi bir
bloktur, blok bir ifadedir ve bir ifadenin ürettiği değer — burada da her zamanki gibi —
son satırın noktalı virgül taşıyıp taşımadığına bağlıdır. Kaynaktaki en küçük birim (tek bir
satır) ile en büyük birimlerden biri (bir fonksiyonun tamamı) aynı kuralla okunabiliyor;
kursun başında kurulan sınıflandırma, ölçeği büyüdükçe yeni bir istisna kazanmıyor.
Özet
- Bir fonksiyonun gövdesi bir bloktur; blok zaten bir ifadedir ve son satırı noktalı
virgülsüzse o satırın değerini, noktalı virgüllüyse
()i üretir — fonksiyonun dönüş değeri bu üretilen değerdir. - Gövdenin ürettiği değerin tipi, fonksiyonun bildirdiği dönüş tipiyle birebir eşleşmek zorundadır; bu eşleşme derleme zamanında denetlenir.
- Noktalı virgül bir hesaplamayı iptal etmez, yalnız sonucunu sessizce atar; atılan hesaplamanın içindeki yan etkiler yine de gerçekleşir.
returnyalnız gövdenin doğal sonundan önce çıkmak için gerekir; bütün dallanma tek bir tail ifadeye toplanabiliyorsareturnhiç yazılmaz.- Aynı sonucu üreten iki yazım (erken çıkışlı ve tail-ifadeli) eşit derecede geçerlidir; seçim okunabilirlikle ilgilidir, doğrulukla değil.
Sonraki Adım
Bu derste if/else if/else zinciri zaten bir ifade olarak kullanıldı, ama denetim
akışının beş biçiminin (if, match, loop, while, for) hangilerinin değer ürettiği
tek tek ölçülmedi. Sıradaki ders bu beşini yan yana koşturur ve bir ifin dallarının neden
hep aynı tipte olmak zorunda olduğunu, bir döngünün ne zaman değer üretip ne zaman hiç
üretmediğini gösterir. isaretle_ifadeylenin üç dalının da i32 döndürmesi bir rastlantı
değildi — dallardan biri farklı bir tip dönseydi bu gövde hiç derlenmezdi; bu kısıtın
kaynağı sıradaki dersin konusu.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.