D Programlama Dili – Programlama dersleri ve D referansı
Ali Çehreli

birim testi: [unit test], programın alt birimlerinin bağımsız olarak denetlenmeleri
çıkış koşulu: [postcondition], işlevin garanti ettiği sonuç
dönüş değeri: [return value], işlevin üreterek döndürdüğü değer
giriş koşulu: [precondition], işlevin gerektirdiği koşul
işlev: [function], programdaki bir kaç adımı bir araya getiren program parçası
parametre: [parameter], işleve işini yapması için verilen bilgi
sözleşme ifadeleri: [expression-based contracts], sözleşme kısa söz dizimi
sözleşmeli programlama: [contract programming], işlevlerin giriş çıkış koşullarını ve nesnelerin tutarlılığını denetleyen dil olanağı
... bütün sözlük



İngilizce Kaynaklar


Diğer




Sözleşmeli Programlama

Sözleşmeli programlama, işlevlerin hizmet sunan birimler olarak kabul edilmeleri düşüncesi üzerine kurulu bir programlama yöntemidir. Bu düşünceye göre, işlevler ve onları çağıran kodlar arasında yazısız bazı anlaşmalar vardır. Sözleşmeli programlama, bu anlaşmaları dil düzeyinde belirlemeye yarayan olanaktır.

Sözleşmeli programlama, ticari bir dil olan Eiffel tarafından "design by contract (DBC)" adıyla yayılmıştır. Bu yöntem D dilinde "contract programming" olarak geçer. Birim testlerinde olduğu gibi, assert denetimlerine dayanır ve D'nin kod sağlamlığı sağlayan bir başka olanağıdır.

D'de sözleşmeli programlama üç temelden oluşur:

invariant bloklarını ve sözleşme kalıtımını ilerideki bir bölümde ve yapı ve sınıflardan daha sonra göreceğiz.

Giriş koşulları için in blokları

İşlevlerin doğru çalışabilmeleri, aldıkları parametre değerlerine bağlı olabilir. Örneğin karekök alan bir işlev kendisine verilen parametrenin sıfırdan küçük olmamasını şart koşar; veya parametre olarak tarih bilgisi alan bir işlev ayın 1 ile 12 arasında olmasını şart koşar.

Bu tür koşulları daha önce assert ve enforce bölümünde görmüştük. İşlevlerin parametreleriyle ilgili olan assert denetimleri işlevin tanımlandığı blok içinde yapılıyordu:

string zamanDizgisi(int saat, int dakika) {
    assert((saat >= 0) && (saat <= 23));
    assert((dakika >= 0) && (dakika <= 59));

    return format("%02s:%02s", saat, dakika);
}

D'nin sözleşmeli programlama anlayışında işlevlerin giriş koşulları "giriş" anlamına gelen in bloklarında denetlenir. Sözleşmeli programlama blokları kullanıldığı zaman, işlevin asıl bloğu da do ile belirlenir:

import std.stdio;
import std.string;

string zamanDizgisi(int saat, int dakika)
in {
    assert((saat >= 0) && (saat <= 23));
    assert((dakika >= 0) && (dakika <= 59));

} do {
    return format("%02s:%02s", saat, dakika);
}

void main() {
    writeln(zamanDizgisi(12, 34));
}

Not: D'nin önceki sürümlerinde bu amaç için do yerine body anahtar sözcüğü kullanılırdı.

İşlevin in bloğunun yararı, işlevin başlatılmasıyla ilgili olan denetimlerin bir arada ve ayrı bir blok içinde yapılmasıdır. Böylece assert denetimleri işlevin asıl işlemlerinin arasına karışmamış olurlar. İşlevin içinde yine de gerektikçe assert denetimleri kullanılabilir, ama giriş koşulları sözleşmeli programlama anlayışına uygun olarak in bloğuna yazılırlar.

in bloklarındaki kodlar programın çalışması sırasında işlevin her çağrılışında otomatik olarak işletilirler. İşlevin asıl işleyişi, ancak bu koşullar sağlandığında devam eder. Böylece işlevin geçersiz başlangıç koşulları ile çalışması ve programın yanlış sonuçlarla devam etmesi önlenmiş olur.

in bloğundaki bir assert denetiminin başarısız olması sözleşmeyi işlevi çağıran tarafın bozduğunu gösterir. İşlev sözleşmenin gerektirdiği şekilde çağrılmamış demektir.

Çıkış garantileri için out blokları

İşlevin yaptığı kabul edilen sözleşmenin karşı tarafı da işlevin sağladığı garantilerdir. Örneğin belirli bir senedeki Şubat ayının kaç gün çektiği bilgisini döndüren bir işlevin çıkış garantisi, döndürdüğü değerin 28 veya 29 olmasıdır.

Çıkış garantileri, işlevlerin "çıkış" anlamına gelen out bloklarında denetlenirler.

İşlevin dönüş değerinin özel bir ismi yoktur; bu değer return ile isimsiz olarak döndürülür. Bu durum, dönüş değeriyle ilgili garantileri yazarken bir sorun doğurur: ismi olmayınca, dönüş değeriyle ilgili assert denetimleri de yazılamaz.

Bu sorun out anahtar sözcüğünden sonra verilen isimle halledilmiştir. Bu isim dönüş değerini temsil eder ve denetlenecek olan garantilerde bu isim kullanılır:

int şubattaKaçGün(int yıl)
out (sonuç) {
    assert((sonuç == 28) || (sonuç == 29));

} do {
    return artıkYıl_mı(yıl) ? 29 : 28;
}

Ben out bloğunun parametresinin ismi olarak sonuç yazmayı uygun buldum; siz dönüşDeğeri gibi başka bir isim de verebilirsiniz. Hangi ismi kullanırsanız kullanın, o isim işlevin dönüş değerini temsil eder.

Bazen işlevin dönüş değeri yoktur, veya dönüş değerinin denetlenmesi gerekmiyordur. O zaman out bloğu parametresiz olarak yazılır:

out {
    // ...
}

İşleve girerken in bloklarının otomatik olarak işletilmeleri gibi, out blokları da işlevden çıkarken otomatik olarak işletilirler.

out bloğundaki bir assert denetiminin başarısız olması sözleşmenin işlev tarafından bozulduğunu gösterir. İşlev sözleşmenin gerektirdiği değeri veya yan etkiyi üretememiş demektir.

Daha önceki bölümlerde hiç kullanmamış olduğumuzdan da anlaşılabileceği gibi, in ve out bloklarının kullanımı seçime bağlıdır. Bunlara yine seçime bağlı olan unittest bloklarını da eklersek, D'de işlevler dört blok halinde yazılabilirler:

Bütün bu blokları içeren bir işlev tanımı şöyle yazılabilir:

import std.stdio;

/* Toplamı iki parça olarak bölüştürür.
 *
 * Toplamdan öncelikle birinciye verir, ama birinciye hiçbir
 * zaman 7'den fazla vermez. Gerisini ikinciye verir. */
void bölüştür(int toplam, out int birinci, out int ikinci)
in {
    assert(toplam >= 0, "toplam sıfırdan küçük olamaz");

} out {
    assert(toplam == (birinci + ikinci));

} do {
    birinci = (toplam >= 7) ? 7 : toplam;
    ikinci = toplam - birinci;
}

unittest {
    int birinci;
    int ikinci;

    // Toplam 0 ise ikisi de 0 olmalı
    bölüştür(0, birinci, ikinci);
    assert(birinci == 0);
    assert(ikinci == 0);

    // Toplam 7'den az ise birincisi toplam'a, ikincisi 0'a
    // eşit olmalı
    bölüştür(3, birinci, ikinci);
    assert(birinci == 3);
    assert(ikinci == 0);

    // Sınır koşulunu deneyelim
    bölüştür(7, birinci, ikinci);
    assert(birinci == 7);
    assert(ikinci == 0);

    // 7'den fazla olduğunda birinci 7 olmalı, gerisi ikinciye
    // gitmeli
    bölüştür(8, birinci, ikinci);
    assert(birinci == 7);
    assert(ikinci == 1);

    // Bir tane de büyük bir değerle deneyelim
    bölüştür(1_000_007, birinci, ikinci);
    assert(birinci == 7);
    assert(ikinci == 1_000_000);
}

void main() {
    int birinci;
    int ikinci;

    bölüştür(123, birinci, ikinci);
    writeln("birinci: ", birinci, " ikinci: ", ikinci);
}

Program aşağıdaki gibi derlenebilir ve çalıştırılabilir:

$ dmd deneme.d -w -unittest
$ ./deneme
birinci: 7 ikinci: 116

Bu işlevin asıl işi yalnızca 2 satırdan oluşuyor; denetleyen kodlar ise tam 19 satır! Bu kadar küçük bir işlev için bu kadar emeğin gereksiz olduğu düşünülebilir. Ama dikkat ederseniz, programcı hiçbir zaman bilerek hatalı kod yazmaz. Programcının yazdığı kod her zaman için doğru çalışacak şekilde yazılmıştır. Buna rağmen, hatalar da hep böyle doğru çalışacağı düşünülen kodlar arasından çıkar.

İşlevlerden beklenenlerin birim testleri ve sözleşmeli programlama ile böyle açıkça ortaya koyulmaları, doğru olarak yazdığımız işlevlerin her zaman için doğru kalmalarına yardım eder. Program hatalarını azaltan hiçbir olanağı küçümsememenizi öneririm. Birim testleri ve sözleşmeli programlama olanakları bizi zorlu hatalardan koruyan çok etkili araçlardır. Böylece zamanımızı hata ayıklamak yerine, ondan çok daha zevkli ve verimli olan kod yazmaya ayırabiliriz.

Sözleşme ifadeleri

in ve out bloklarının her çeşit D kodu içerebilmesi doğal olarak yararlı bir olanaktır. Buna rağmen, giriş koşulları ve çıkış garantileri çoğu zaman basit assert ifadelerinden oluşurlar. Böyle durumlarda bir kolaylık olarak sözleşme ifadelerinden yararlanılabilir. Aşağıdaki işleve bakalım:

int işlev(int a, int b)
in {
    assert(a >= 7, "a 7'den küçük olamaz");
    assert(b < 10);

} out (sonuç) {
    assert(sonuç > 1000);

} do {
    // ...
}

Sözleşme ifadeleri blok parantezlerini ortadan kaldırır, assert açıkça çağrılmaz, ve do anahtar sözcüğüne gerek kalmaz:

int işlev(int a, int b)
in (a >= 7, "a 7'den küçük olamaz")
in (b < 10)
out (sonuç; sonuç > 1000) {
    // ...
}

İşlevin dönüş değerinin out ifadesindeki noktalı virgülden önce isimlendirildiğine dikkat edin. İşlevin dönüş değeri bulunmadığında veya çıkış garantisi dönüş değeri ile ilgili olmadığında bu noktalı virgül yine de yazılmalıdır:

out (; /* ... */)
Sözleşmeli programlamayı etkisizleştirmek

Birim testlerinin tersine, sözleşmeli programlama normalde etkilidir; etkisizleştirmek için özel bir derleyici veya geliştirme ortamı ayarı gerekir. Bunun için dmd derleyicisinde -release seçeneği kullanılır:

dmd deneme.d -w -release

Program o seçenekle derlendiğinde in, out, ve invariant blokları programa dahil edilmezler.

in bloğu mu enforce mu

assert ve enforce bölümünde karşılaştığımız assert ile enforce arasındaki karar güçlüğü in blokları ile enforce() arasında da vardır. in bloğundaki assert denetimlerinin mi yoksa işlev tanımı içindeki enforce denetimlerinin mi daha uygun olduğuna karar vermek bazen güç olabilir.

Yukarıda gördüğümüz gibi, sözleşmeli programlama bütünüyle etkisizleştirilebilir. Bundan da anlaşılabileceği gibi, sözleşmeli programlama da assert ve unittest gibi programcı hatalarına karşı koruma getiren bir olanaktır.

Bu yüzden işlevlerin giriş koşulu denetimlerinin hangi yöntemle sağlanacağının kararı da yine assert ve enforce bölümünde gördüğümüz maddelerle verilebilir:

Problem

İki futbol takımının puanlarını bir maçın sonucuna göre arttıran bir işlev yazın.

Bu işlevin ilk iki parametresi birinci ve ikinci takımın attıkları goller olsun. Son iki parametresi de bu takımların maçtan önceki puanları olsun. Bu işlev golleri dikkate alarak birinci ve ikinci takımın puanlarını düzenlesin: fazla gol atan taraf üç puan kazansız, goller eşitse iki takım da birer puan kazansınlar.

Ek olarak, işlevin dönüş değeri de kazanan tarafı belirtsin: birinci kazanmışsa 1, ikinci kazanmışsa 2, berabere kalmışlarsa 0.

Aşağıdaki programla başlayın ve işlevin dört bloğunu uygun şekilde doldurun. Benim main içine yazdığım assert denetimlerini silmeyin. Onlar benim bu işlevin çalışması konusundaki beklentilerimi belgeliyorlar.

int puanEkle(int goller1, int goller2,
             ref int puan1, ref int puan2)
in {
    // ...

} out (sonuç) {
    // ...

} do {
    int kazanan;

    // ...

    return kazanan;
}

unittest {
    // ...
}

void main() {
    int birincininPuanı = 10;
    int ikincininPuanı = 7;
    int kazananTaraf;

    kazananTaraf =
        puanEkle(3, 1, birincininPuanı, ikincininPuanı);
    assert(birincininPuanı == 13);
    assert(ikincininPuanı == 7);
    assert(kazananTaraf == 1);

    kazananTaraf =
        puanEkle(2, 2, birincininPuanı, ikincininPuanı);
    assert(birincininPuanı == 14);
    assert(ikincininPuanı == 8);
    assert(kazananTaraf == 0);
}

Ben int seçtiğim halde burada üç değerli bir enum türü döndürmek daha uygun olurdu:

enum MaçSonucu {
    birinciKazandı, ikinciKazandı, berabere
}

MaçSonucu puanEkle(int goller1, int goller2,
                   ref int puan1, ref int puan2)
// ...