İngilizce Kaynaklar


Diğer




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:

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.