Fonksiyon Argümanlarında Tembel Değerlendirmeler
Yazar: Walter Bright
Çeviren: Ali Çehreli
Tarih: 10 Temmuz 2009
İngilizcesi: Lazy Evaluation of Function Arguments
Tembel değerlendirme [lazy evaluation], bir ifadenin işletilmesinin ancak gerçekten gerek duyulana kadar geciktirilmesidir. Tembel değerlendirme geleneksel olarak &&
, ||
, ve ?:
işleçlerinde karşımıza çıkar.
void deneme(int* p) { if (p && p[0]) ... }
İkinci ifade olan p[0]
, ancak p
'nin NULL
'dan farklı olduğu durumda işletilecektir. Eğer ikinci ifade tembel olarak işletilmemiş olsa, p
'nin NULL
olduğu durumda bir çalışma zamanı hatası oluşurdu.
Ne kadar değerli olsalar da tembel işleçlerin ciddi kısıtlamaları vardır. Örneğin çalışması global bir değişkenle yönetilen bir loglama fonksiyonunu ele alalım. Loglamayı bu global değişkenin değerine göre açıp kapatıyor olalım:
void log(char[] mesaj) { if (loglama_açık) fwritefln(log_dosyası, mesaj); }
Yazdırılacak olan mesaj çoğu durumda çalışma zamanında oluşturulur:
void foo(int i) { log("foo(), i'nin " ~ toString(i) ~ " değeriyle çağrıldı"); }
Bu kod istediğimiz gibi çalışıyor olsa da; burada mesaj loglamanın kapalı olduğu zamanlarda bile oluşturulmaktadır. Çok miktarda loglama yapan programlar bu yüzden gereksizce zaman kaybederler.
Bir çözüm, tembel değerlendirmenin programcı tarafından açıkça yapılmasıdır:
void foo(int i) { if (loglama_açık) log("foo(), i'nin " ~ toString(i) ~ " değeriyle çağrıldı"); }
Ancak bu, loglama yönetimiyle ilgili bir değişkeni kullanıcıya gösterdiği için sarma ilkesine aykırı bir kullanımdır. Bunun önüne geçmenin C'deki bir yolu makro tanımlamaktır:
#define LOG(mesaj) (loglama_acik && log(mesaj))
Ama bu çözüm de makroların bilinen eksiklikleri nedeniyle yeni sorunlar doğurur:
loglama_acik
değişkeni kullanıcının isim alanına dahil olmuştur- Makrolar hata ayıklayıcılar tarafından görülemezler
- Makrolar global olarak çalışırlar; bir kapsam içine alınamazlar
- Makrolar sınıf üyeleri olamazlar
- Adresleri alınamadığı için başka fonksiyonlara fonksiyon olarak geçirilemezler
Bunun yerine fonksiyon parametrelerinin tembel olarak işletilmeleri daha sağlam bir çözümdür. D'de bunu yapmanın bir yolu, delegate()
parametreler kullanmaktır:
void log(char[] delegate() dg) { if (loglama_açık) fwritefln(log_dosyası, dg()); } void foo(int i) { log( { return "foo(), i'nin " ~ toString(i) ~ " değeriyle çağrıldı"; } ); }
Bu kodda mesaj yalnızca loglamanın açık olduğu durumda oluşturulacaktır ve bu sefer sarma ilkesi de korunmaktadır. Bu seferki sorun ise, ifadelerin { return ifade; }
olarak yazılmalarını gerektirmesidir.
D bu noktada Andrei Alexandrescu tarafından önerilmiş olan küçük ama önemli bir adım atar: D'de her ifade, dönüş türü void
veya ifadenin kendi türü olan bir delegate
'e otomatik olarak dönüşür. delegate
bildiriminin yerini de Tomasz Stachowiak'ın önerisi olan lazy
türler alır. Fonksiyonların yeni yazımı şöyledir:
void log(lazy char[] dg) { if (loglama_açık) fwritefln(log_dosyası, dg()); } void foo(int i) { log("foo(), i'nin " ~ toString(i) ~ " değeriyle çağrıldı"); }
Tekrar en baştaki kullanıma dönmüş olduk, ama burada ifade ancak loglama açık olduğunda işletilmektedir.
Kodda karşılaşılan yazım veya tasarım tekrarlarını soyutlamak ve bir şekilde sarmalamak, hem kodun karmaşıklığını hem de hata risklerini azaltır. Bunun en güzel örneklerinden birisi fonksiyon kavramının ta kendisidir. Tembel değerlendirme kavramı ise bir sürü başka yöntemin soyutlanabilmesini sağlar.
Basit bir örnek olarak tekrar
kere tekrarlanması gereken bir ifadeyi ele alalım. Bunun en klasik yazımı şöyledir:
for (int i = 0; i < tekrar; i++) ifade;
O yöntemi tembel değerlendirme kullanarak şu şekilde sarmalayabiliriz:
void tekrarla(int tekrar, lazy void ifade) { for (int i = 0; i < tekrar; i++) ifade(); }
ve şöyle kullanabiliriz:
void foo() { int x = 0; tekrarla(10, writef(x++)); }
Çıktısı bekleneceği gibi şu şekildedir:
0123456789
Daha karmaşık kontrol yapıları da kurulabilir. Örneğin şöyle switch
benzeri bir yapı oluşturulabilir:
bool eğer(bool b, lazy void dg) { if (b) dg(); return b; } /* Buradaki belirsiz argümanlar bu özel durumda bir delegate dizisine dönüşürler. */ void seçenekler(bool delegate()[] kontroller...) { foreach (c; kontroller) { if (c()) break; } }
ve şöyle kullanılabilir:
void foo() { int v = 2; seçenekler ( eğer(v == 1, writefln("değeri 1")), eğer(v == 2, writefln("değeri 2")), eğer(v == 3, writefln("değeri 3")), eğer(true, writefln("varsayılan değer")) ); }
Bunun çıktısı şöyle olur:
değeri 2
Burada Lisp programlama dilinin makrolarıyla olan benzerlikleri farketmiş olabilirsiniz.
Son bir örnek olarak şu çok karşılaşılan kod örüntüsüne [pattern] bakalım:
Abc p; p = foo(); if (!p) throw new Hata("foo() başarısız"); p.bar(); // p'yi şimdi kullanabiliriz
throw
ifade değil, bir deyim olduğu için; bunu kullanan ifadelerin birden fazla satırda yazılmaları ve değişken tanımlanması gerekmektedir. (Bu konuda daha ayrıntılı bilgi için Andrei Alexandrescu ve Petru Marginean'ın Enforcements makalesini okuyabilirsiniz.) Oysa bütün bu kod tembel değerlendirme kullanılarak bir fonksiyon içine alınabilir:
Abc Güvenli(Abc p, lazy char[] mesaj) { if (!p) throw new Hata(mesaj()); return p; }
ve biraz önceki kod şöyle basit bir hale gelir:
Güvenli(foo(), "foo() başarısız").bar();
ve 5 satırlık kod tek satıra indirgenmiş olur. Hattâ bir şablona dönüştürülürse Güvenli
çok daha kullanışlı bir hale getirilebilir:
T Güvenli(T)(T p, lazy char[] mesaj) { if (!p) throw new Hata(mesaj()); return p; }
Sonuç
Fonksiyon argümanlarının tembel olarak işletilebilmeleri fonksiyonların ifade yeteneklerini olağanüstü bir biçimde arttırmaktadır. Çok kullanılan kod örüntülerinin daha önceden hantal veya olanaksız olan sarmalanmaları da bu sayede olanaklı hale gelmektedir.
Teşekkür
Andrei Alexandrescu, Bartosz Milewski, ve David Held bana bu konuda hem ilham kaynağı oldular hem de büyük yardımda bulundular. Kendilerine içten teşekkür ederim. D camiasının da Tomasz Stachowiak'ın açtığı bir konuda da görüldüğü gibi yapıcı eleştirileri olmuştur.