D.ershane D Programlama Dili Dersleri

atama: [assign], değişkene yeni bir değer vermek
değişken: [variable], kavramları temsil eden veya sınıf nesnesine erişim sağlayan program yapısı
dönüş değeri: [return value], işlevin üreterek döndürdüğü değer
fonksiyonel programlama: [functional programming], yan etki üretmeyen programlama yöntemi
işlev: [function], programdaki bir kaç adımı bir araya getiren program parçası
nesne: [object], belirli bir sınıf veya yapı türünden olan değişken
parametre: [parameter], işleve işini yapması için verilen bilgi
referans: [reference], asıl nesneye, onun takma ismi gibi erişim sağlayan program yapısı
yapı: [struct], başka verileri bir araya getiren veri yapısı
... bütün sözlük

Bölümler
İngilizce Kaynaklar
Diğer



İşlev Parametreleri

Bu derste parametrelerin işlevlere gönderilmeleri konusundaki önemli ayrıntıları göstereceğim ve D'deki parametre çeşitlerini tanıtacağım.

Aslında bu konunun bir kısmına foreach Döngüsü dersinde ref anahtar sözcüğünü anlatırken çok kısaca değinmiştik: o sözcük kullanılmadığında, foreach içinde dizi elemanlarının kendilerine değil, kopyalarına erişiyorduk.

Ek olarak, const ve immutable'ın parametrelerle kullanımını da bir önceki derste görmüştük.

Önceki programlarda işlevlerin parametrelerini kullanarak nasıl sonuç ürettiklerini gördük. Örneğin hiçbir yan etkisi olmayan ve işi yalnızca değer üretmek olan bir işlev şöyle yazılabiliyordu:

double seneSonuNotu(double vizeNotu, double finalNotu)
{
    const double sonuç = vizeNotu * 0.4 + finalNotu * 0.6;
    return sonuç;
}

O işlevde vize notu ağırlığının %40, final notununkinin de %60 olarak hesaplandığını görebiliyoruz. O işlevi örneğin şu şekilde çağırabiliriz:

    int vizeOrtalaması = 76;
    int finalNotu = 80;

    writefln("Sene sonu notu: %2.0f",
             seneSonuNotu(vizeOrtalaması, finalNotu));
Çoğu parametre kopyalanır

Yukarıdaki kodun vizeOrtalaması ve finalNotu değişkenlerini kullandığını söylediğimizde aslında temelde bir hataya düşmüş oluruz; çünkü aslında işlev tarafından kullanılanlar değişkenlerin kendileri değil, kopyalarıdır.

Bu ayrım önemlidir; çünkü parametrede yapılan değişiklik ancak kopyayı etkiler. Bunu yan etki üretmeye çalışan aşağıdaki işlevde görebiliriz. Bir oyun karakterinin enerjisini düşürmek için yazılmış olsun:

void enerjisiniDüşür(double enerji)
{
    enerji /= 4;
}

O işlevi denemek için yazılmış olan şu programa bakalım:

import std.stdio;

void enerjisiniDüşür(double enerji)
{
    enerji /= 4;
}

void main()
{
    double enerji = 100;

    enerjisiniDüşür(enerji);
    writeln("Yeni enerji: ", enerji);
}
Yeni enerji: 100     ← Değişmedi

enerjisiniDüşür işlevi, parametresinin değerini dörtte birine düşürdüğü halde main içindeki enerji isimli değişkenin değeri aynı kalmaktadır. Bunun nedeni, main içindeki enerji ile enerjisiniDüşür işlevinin parametresi olan enerji'nin farklı değişkenler olmalarıdır. Parametre olan, main içindeki değişkenin kopyasıdır.

Bu olayı biraz daha yakından incelemek için programa bazı çıktı ifadeleri yerleştirebiliriz:

import std.stdio;

void enerjisiniDüşür(double enerji)
{
    writeln("İşleve girildiğinde       : ", enerji);
    enerji /= 4;
    writeln("İşlevden çıkılırken       : ", enerji);
}

void main()
{
    double enerji = 100;

    writeln("İşlevi çağırmadan önce    : ", enerji);
    enerjisiniDüşür(enerji);
    writeln("İşlevden dönüldükten sonra: ", enerji);
}
İşlevi çağırmadan önce    : 100
İşleve girildiğinde       : 100
İşlevden çıkılırken       : 25   ← parametre değişir,
İşlevden dönüldükten sonra: 100  ← asıl enerji değişmez

Çıktıdan anlaşıldığı gibi, isimleri aynı olsa da, main içindeki enerji ile enerjisiniDüşür içindeki enerji farklı değişkenlerdir. İşleve main içindeki değişkenin değeri kopyalanır ve değişiklik bu kopyayı etkiler.

Bu, ilerideki derslerde göreceğimiz yapı nesnelerinde de böyledir: yapı nesneleri de işlevlere kopyalanarak gönderilirler.

Dizi ve sınıf nesnesi olan parametreler kopyalanmazlar

Diziler ve daha sonraki derslerde göreceğimiz sınıf nesneleri, parametrelerin kopyalanmaları kuralına uymazlar. Bu tür değişkenler işlevlere referans olarak geçirilirler. Referans olarak geçirilen parametre, asıl nesnenin bir takma ismi gibi kullanılır ve parametrede yapılan değişiklik asıl nesneyi değiştirir.

Dizgiler de dizi olduklarından, bu durum onlar için de geçerlidir. Parametresinde değişiklik yapan şu işleve bakalım:

import std.stdio;

void başHarfiniNoktaYap(char[] dizgi)
{
    dizgi[0] = '.';
}

void main()
{
    char[] dizgi = "abc".dup;
    başHarfiniNoktaYap(dizgi);
    writeln(dizgi);
}

Parametrede yapılan değişiklik, main içindeki asıl nesneyi değiştirmiştir:

.bc

Çünkü; işlevin parametresi olan dizgi, main içindeki asıl dizgi'nin takma ismidir.

Parametre çeşitleri

Parametrelerin işlevlere geçirilmeleri normalde yukarıdaki iki temel kurala uyar:

Bunlar, bir belirteç kullanılmadığı zaman varsayılan kurallardır. Aşağıdaki anahtar sözcükleri kullanarak bu kuralları etkileyebiliriz.

in

İşlevlerin değer veya yan etki üreten düzenekler olduklarını kabul etmiş ve o düzeneğin işleyişini parametrelerin belirlediğini görmüştük.

in anahtar sözcüğü, parametrenin bu düzenekte yalnızca bir giriş bilgisi olarak kullanılacağını belirler. Bu tür parametreler işlevde yalnızca bilgi olarak kullanılırlar, kendileri değiştirilemezler. in bu açıdan const'a benzese de, İngilizce'de "içeriye" anlamına geldiği için, parametrenin amacını daha açık bir şekilde ifade eder:

import std.stdio;

double ağırlıklıToplam(in double şimdikiToplam,
                       in double ağırlık,
                       in double eklenecekDeğer)
{
    return şimdikiToplam + (ağırlık * eklenecekDeğer);
}

void main()
{
    writeln(ağırlıklıToplam(1.23, 4.56, 7.89));
}

in parametreler değiştirilemezler:

void deneme(in int değer)
{
    değer = 1;    // ← derleme HATASI
}

Giriş amacıyla kullanılan parametreleri in anahtar sözcüğü ile belirtmenizi öneririm. İşlevlerinizin daha okunaklı ve daha güvenli olmalarına yardım eder.

out

İşlevin ürettiği bilginin işlevden dönüş değeri olarak döndürüldüğünü görmüştük. İşlevlerden yalnızca tek bir bilgi döndürülebiliyor olması, bazen kısıtlayıcı olabilir; bazen işlevin birden fazla sonuç üretmesini isteyebiliriz.

"Dışarıya" anlamına gelen out belirteci, işlevlerin parametreleri yoluyla da sonuç üretmelerini sağlar. İşlev, bu tür parametrelerin değerlerini atama yoluyla değiştirdiğinde, o değerler işlevi çağıran ortamda da sonuç olarak görülebilirler. Bir anlamda bilgi işlevden dışarıya gönderilmektedir.

Not: Aslında dönüş türü olarak birden fazla değer de döndürülebilir. Bunu, dönüş türünü bir Tuple veya struct türü olarak belirleyerek gerçekleştirebiliriz. Bunları da ilerideki derslerde göreceğiz.

Örnek olarak iki sayıyı bölen, ve hem bölümü hem de kalanı üreten bir işleve bakalım. İşlevin dönüş değerini bölümü döndürmek için kullanırsak, kalanı da bir out parametre olarak döndürebiliriz:

import std.stdio;

int kalanlıBöl(in int bölünen, in int bölen, out int kalan)
{
    kalan = bölünen % bölen;
    return bölünen / bölen;
}

void main()
{
    int bölüm;
    int kalan;

    bölüm = kalanlıBöl(7, 3, kalan);

    writeln("bölüm: ", bölüm, ", kalan: ", kalan);
}

kalan parametresini işlev içinde değiştirmek, main içindeki kalan'ın değişmesine neden olmaktadır:

bölüm: 2, kalan: 1

out parametreler yalnızca dışarıya bilgi verirler; o parametrelerin değeri çağıran tarafta ne olursa olsun, işleve girildiğinde her zaman için türünün ilk değerine dönüşür:

import std.stdio;

void deneme(out int parametre)
{
    writeln("İşleve girildiğinde   : ", parametre);
}

void main()
{
    int değer = 100;

    writeln("İşlev çağrılmadan önce: ", değer);
    deneme(değer);
    writeln("İşlevden dönüldüğünde : ", değer);
}

O işlevde parametreye hiçbir değer atanmıyor bile olsa, işleve girildiğinde parametrenin değeri int'in ilk değeri olmakta ve bu main içindeki değeri de etkilemektedir:

İşlev çağrılmadan önce: 100
İşleve girildiğinde   : 0     ← int'in ilk değerinde
İşlevden dönüldüğünde : 0

Yani out parametreler dışarıdan bilgi alamazlar, yalnızca dışarıya bilgi gönderebilirler.

out parametreler yerine, dönüş türü olarak Tuple veya struct türleri kullanmayı da düşünebilirsiniz. O olanaklar, birden fazla bilgiyi bir arada döndürmeyi sağlarlar.

const

Bir önceki derste de gördüğümüz gibi, parametrenin işlev içerisinde değiştirilmeyeceği garantisini verir. Bu sayede, işlevi çağıranlar hem parametrede değişiklik yapılmadığını bilmiş olurlar, hem de işlev const veya immutable olan değişkenlerle de çağrılabilir:

import std.stdio;

char sonHarfi(const char[] dizgi)
{
    return dizgi[$ - 1];
}

void main()
{
    writeln(sonHarfi("sabit"));
}

Burada bir seçim yapmak durumundasınız: in ve const'ın ikisi de parametre listesinde aynı anlama geldikleri için birisinde karar kılmak isteyebilirsiniz. in'in yararı, anlamı "içeriye" olduğu için amacını daha açık bir şekilde ifade etmesidir. const'ın yararı ise, parametre listeleri dışında da kullanılabilen bir anahtar sözcük olduğu için değişmezlik kavramını belirtme konusunda tutarlılık sağlıyor olmasıdır. Seçim sizin...

immutable

Bir önceki derste de gördüğümüz gibi, parametrenin programın çalışması süresince değişmemesini şart koşar. Bu konuda ısrarlı olduğu için, aşağıdaki işlevi ancak immutable dizgilerle çağırabiliriz (örneğin dizgi hazır değerleriyle):

import std.stdio;

dchar[] karıştır(immutable dchar[] birinci,
                 immutable dchar[] ikinci)
{
    dchar[] sonuç;
    int i;

    for (i = 0; (i < birinci.length) && (i < ikinci.length);
         ++i) {
        sonuç ~= birinci[i];
        sonuç ~= ikinci[i];
    }

    sonuç ~= birinci[i..$];
    sonuç ~= ikinci[i..$];

    return sonuç;
}

void main()
{
    writeln(karıştır("MERHABA", "dünya"));
}

Kısıtlayıcı bir belirteç olduğu için, immutable'ı ancak gerçekten gereken durumlarda ve kendi işlevlerinizde belki de hiçbir zaman kullanmayacaksınız.

ref

İşleve normalde kopyalanarak geçirilecek olan bir parametrenin referans olarak geçirilmesini sağlar.

Yukarıda parametresi normalde kopyalandığı için istediğimiz gibi çalışmayan enerjisiniDüşür işlevinin; main içindeki asıl değişkeni değiştirebilmesi için, parametresini referans olarak alması gerekir:

import std.stdio;

void enerjisiniDüşür(ref double enerji)
{
    enerji /= 4;
}

void main()
{
    double enerji = 100;

    enerjisiniDüşür(enerji);
    writeln("Yeni enerji: ", enerji);
}

Bu sayede; işlev parametresinde yapılan değişiklik, main içindeki enerji'nin değerini değiştirir:

Yeni enerji: 25

Görüldüğü gibi, ref parametreler işlev içinde hem kullanılmak üzere giriş bilgisidirler, hem de sonuç üretmek üzere çıkış bilgisidirler...

ref parametreler işlevlerin yan etki üreten türden işlevler olmalarına neden olurlar: Dikkat ederseniz, enerjisiniDüşür işlevi değer üretmiyor, parametresinde bir değişiklik yapıyor.

İşlevsel programlama [functional programming] denen programlama mantığında yan etkilerin özellikle azaltılmasına çalışılır; hatta bazı programlama dillerinde yan etkilere izin verilmez bile. Değer üreten işlevlerin yan etkisi olan işlevlerden programcılık açısından daha üstün oldukları kabul edilir. Ben de, işlevlerinizi eğer olabiliyorsa değer üretecek şekilde tasarlanamınızı öneririm. İşlevlerin yan etkilerini azaltmak, programlarınızın güvenilirliği açısından etkili olacaktır.

Aynı işi fonksiyonel programlama mantığa uygun olacak şekilde gerçekleştirmek için, yani değer üreten bir işlev kullanacak şekilde yazmak için, programı şöyle değiştirmek önerilir:

import std.stdio;

double düşükEnerji(double enerji)
{
    return enerji / 4;
}

void main()
{
    double enerji = 100;

    enerji = düşükEnerji(enerji);
    writeln("Yeni enerji: ", enerji);
}
lazy

Şimdiye kadar, parametre değerlerinin işlevlere gönderilmeden önce işletildiklerini söyleme gereği duymadım. Bunun size de doğal geldiğini düşünürüm. Örneğin topla gibi bir işlevi başka iki işlevin sonucu ile çağırsak:

    sonuç = topla(birMiktar(), başkaBirMiktar());

topla'nın çağrılabilmesi için öncelikle birMiktar ve başkaBirMiktar işlevlerinin çağrılmaları gerekir. Yoksa topla hangi iki değeri toplayacağını bilemez.

İşlemlerin bu şekilde doğal olarak işletilmeleri hevesli [eager] olarak tanımlanır. Program, işlemleri öncelik sıralarına göre hevesle işletir.

Oysa bazı parametreler, işlevin işleyişine bağlı olarak belki de hiçbir zaman kullanılmayacaklardır. Parametre değerlerinin hevesli olarak önceden işletilmeleri, böyle parametrelerin gereksiz yere hesaplanmış olmalarına neden olacaktır.

Örnek olarak, parametrelerinden birisinin değerini ancak belirli bir koşul sağlandığında kullanan bir işleve bakalım. Aşağıdaki işlev, istenen sayıdaki yumurtayı öncelikle dolapta bulunanlarla yapmaya çalışıyor; komşularda kaç yumurta bulunduğuna ise ancak dolapta yeterince yumurta bulunmadığında bakıyor:

void yumurtaYap(in int istenen,
                in int dolaptaOlan,
                in int komşulardaOlan)
{
    if (istenen <= dolaptaOlan) {
        writeln("Hepsini dolaptaki yumurtalarla yaparım");

    } else if (istenen <= (dolaptaOlan + komşulardaOlan)) {
        writeln("Dolaptaki ", dolaptaOlan, " yumurtaya",
                " komşulardan da ", istenen - dolaptaOlan,
                " yumurta eklerim");

    } else {
        writeln("O kadar yumurta yapamam");
    }
}

Buna ek olarak, komşularda bulunan yumurta adedinin bir hesap sonucunda bilinebildiğini varsayalım. Komşulardaki yumurta sayısı bir değişkenin değeri olarak hemen bilinmek yerine, belirli bir hesap sonrasında bulunuyor olsun:

int toplamYumurta(in int[string] adetler)
{
    int toplam;

    foreach (sahip, adet; adetler) {
        writeln(sahip, ": ", adet, " tane");
        toplam += adet;
    }

    return toplam;
}

O işlev; kendisine verilen bir eşleme tablosunda ilerliyor, yumurta sahibini ve o kişide kaç yumurta bulunduğunu çıktıya yazdırıyor, ve bütün yumurtaların toplamını döndürüyor.

Şimdi yumurtaYap işlevini toplamYumurta'yı kullanarak şöyle çağırabiliriz:

void main()
{
    int[string] komşulardakiYumurtalar =
    [
        "Zümrüt Teyze":5, "Yakup Abi":3, "Veli Efendi":7
    ];

    yumurtaYap(2, 5, toplamYumurta(komşulardakiYumurtalar));
}

Programın çıktısında görüldüğü gibi, önce toplamYumurta işlevi işletilmektedir ve ondan sonra yumurtaYap çağrılmaktadır:

Zümrüt Teyze: 5 tane     
Veli Efendi: 7 tane      ⎬ komşulardaki yumurta hesabı
Yakup Abi: 3 tane        
Hepsini dolaptaki yumurtalarla yaparım

Ancak bu durumda gereksiz bir işlem var: sonuçta bütün yumurtalar dolaptakilerle yapılabilecek olduğu halde, gereksizce komşulardaki yumurtaları da saymış olduk.

Kelime anlamı "tembel" olan lazy, bir parametrenin işlev içinde belki de hiç işletilmeyeceğini bildirmek için kullanılır. yumurtaYap işlevinin komşulardaOlan parametresinin her durumda hesaplanmasına gerek olmadığını ve ancak gerektiğinde hesaplanabileceğini bildirmek için, onu lazy olarak belirleyebiliriz:

void yumurtaYap(in int istenen,
                in int dolaptaOlan,
                lazy int komşulardaOlan)
{
   // ... gerisi aynı ...

Şimdiki çıktıda görüldüğü gibi, istenen yumurta adedinin dolaptakilerden karşılanabildiği yukarıdaki durumda, komşulardaki yumurta toplamı artık hesaplanmamaktadır:

Hepsini dolaptaki yumurtalarla yaparım

O hesap yine de gerektiği zaman yapılacaktır. Örneğin dolapta bulunanlardan daha fazla yumurta istendiğinde:

    yumurtaYap(9, 5, toplamYumurta(komşulardakiYumurtalar));

Bu sefer işlev içinde gerçekten gerektiği için, komşulardaki yumurta sayısı yine hesaplanmaktadır:

Zümrüt Teyze: 5 tane
Veli Efendi: 7 tane
Yakup Abi: 3 tane
Dolaptaki 5 yumurtaya komşulardan da 4 yumurta eklerim
lazy parametre, her erişildiğinde hesaplanır

lazy parametrelerin değerleri, o değer her kullanıldığında tekrar hesaplanır.

Örneğin bu programda lazy olarak belirtilmiş olan parametre üç kere kullanıldığı için, onu hesaplayan işlev de üç kere çağrılmaktadır:

import std.stdio;

int parametreyiHesaplayanİşlev()
{
    writeln("Hesap yapılıyor");
    return 1;
}

void tembelParametreliİşlev(lazy int değer)
{
    int sonuç = değer + değer + değer;
    writeln(sonuç);
}

void main()
{
    tembelParametreliİşlev(parametreyiHesaplayanİşlev());
}
Hesap yapılıyor
Hesap yapılıyor
Hesap yapılıyor
3

lazy belirtecini, değeri ancak bazı durumlarda kullanılan parametreleri belirlemek için kullanabilirsiniz. Ancak, değerin birden fazla sayıda hesaplanabileceğini de unutmamak gerekir.

Bu belirtecin sağladığı tembel değerlendirmeler olanağına bir sonraki derste de devam edeceğim.

Problem

... çözüm