May 22, 2010

Yeni öğrendiğim ve ilginç bulduğum bir yöntemi anlatacağım. Çok uzun oldu; kusura bakmayın! :)

Yöntem şu:

template uçabilir_mi(T)
{
   const bool uçabilir_mi = is (typeof(
   {
       T uçan;            // kurulabilmeli
       uçan.hazırlan();   // uçmaya hazırlanabilmeli
       uçan.uç(1);        // belirli bir mesafe uçabilmeli
       uçan.kon();        // istendiğinde konabilmeli
       uçan.anlat();      // başına gelenleri anlatabilmeli
   }()));
}

(Not: O yöntemi std.range modülünde tanımlanmış olan 'isInputRange''den uyarladım. Böyle kullanımlar Phobos'ta başka modüllerde de var.)

Baştan bana çok garip gelmişti. Dikkat ederseniz, 'typeof''un içinde küme parantezli bazı kodlar var. O küme parantezlerinden hemen sonra da açılıp kapanan işlev parantezleri var: '()'. O kodda neler olduğunu anlayabiliyor musunuz? Benim anlamam biraz zaman aldı... :)

Bu çok sayıda D olanağının birlikte kullanan bir yöntem. Baştan anlamamıştım; çünkü anlamak için gereken her şey Digital Mars'ın sitesinde bile yok (veya ben henüz bulamadım). Olsa da, hiç böyle kullanılacağı aklıma gelmezdi. Üstelik bilmecenin bir parçasını da uykuda çözdüm! :)

Ek olarak, bu yöntemim kesinlikle gerekmediğini de söylemem gerekiyor; ama her usta D'cinin en azından anlaması gerekir.

Yukarıdakilere bakıp da dünyanın en önemli kodlama yöntemi olduğunu da sanmayın lütfen. :) Sadece ilginç bulduğum için yazıyorum. Önemli de değil; günlük programcılıkta da çok kullanılacağını sanmam.

Kullanılan D olanakları:

  • şablonlar <ddili.org/ders/d/sablonlar.html>
  • tek tanım içeren şablonlar (şu sıralarda yazmaktayım)
  • şablon kısıtlamaları (şu sıralarda yazmaktayım)
  • is ifadesi: <ddili.org/ders/d/kosullu_derleme.html>
  • typeof ifadesi: (belirli bir derste anlatmadım ama çok derste geçti)
  • isimsiz kapamalar: <ddili.org/ders/d/kapamalar.html>

Önce sorunu anlayalım. Elimizde şöyle bir işlev şablonu olsun:

void kullan(T)(T nesne)
{
   nesne.hazırlan();
   // ... başka işlemler ...
   nesne.uç(42);
   // ... başka işlemler ...
   nesne.kon();
   // ... başka işlemler ...
   nesne.anlat();
}

O şablonun her türle çağrılabileceği, ama derlenemeyeceği açık: ancak ve ancak şablon içinde çağrılan o dört üye işlevi olan türlerle kullanılabilir.

Örneğin şu türle kullanılabilir:

class ModelUçak
{
   void hazırlan()
   {}

   void uç(int mesafe)
   {}

   void kon()
   {}

   void anlat()
   {}
}

Ama şu türle kullanılamaz:

class Güvercin
{
   void uç(int mesafe)
   {}
}

void main()
{
   auto taklacı = new Güvercin;
   kullan(taklacı);
}

'Güvercin' ile kullanıldığında derleme hataları alırız:

Error: no property 'hazırlan' for type 'deneme.Güvercin'
Error: no property 'kon' for type 'deneme.Güvercin'
Error: no property 'anlat' for type 'deneme.Güvercin'

Güzel, ama sorunlu...

Sorun şu: derleyici, o hatalar için şablonun içindeki satırlara işaret eder. Yani derleyicinin gözünde hata, şablonun içindeki örneğin 'nesne.hazırlan()' çağrısındadır. Bu, C++ şablonlarının da büyük bir sorunudur. Siz bir kod yazarsınız, ondan sonra derleyici standart kütüphanenin bir başlık dosyasının bir satırının derlenemediğini bildirir. Kullanışlı değildir... :/

Oysa yukarıdaki asıl hata; 'kullan''ın, 'main' içinde uygun olmayan bir türle çağrılmasındadır. Doğru olan, derleyicinin o satırda hata vermesidir ama öyle olmamaktadır.

D'de bunun yolu, şablon kısıtlamaları (template constraints) tanımlamaktır. (Bu konu henüz D.ershane'de yok; şu günlerde yazıyorum.) Şablon kısıtlamaları, şablonun kapsamı ile isim satırı arasındaki bir 'if' ifadesiyle belirtilir.

Örneğin yalnızca ve yalnızca uzunluğu 8 bayt olan türlerle çalışabilen bir algoritmamız olsun. Bunu, bir şablon kısıtlaması olarak şöyle ifade edebiliriz:

void algoritma(T)(T nesne)
   if (T.sizeof == 8)       // <-- şablon kısıtlaması
{
   // ...
}

void main()
{
   algoritma(42L);   // derlenir (long 8 bayttır)
   algoritma(1.25);  // derlenir (double 8 bayttır)

   short[4] dizi;
   algoritma(dizi);  // derlenir (bu dizi 4 * 2 == 8 bayttır)

   algoritma(42);    // <-- DERLEME HATASI (int 4 bayttır)
}

Yukarıdaki kısıtlama, derleyicinin o şablonu yalnızca uzunluğu 8 bayt olan türler için göze almasını sağlar. Şablon o kısıtlamaya uymayan türlerle kullanılamaz.

O yüzden, şablonun yukarıdaki 'int' kullanımı bir derleme hatasına neden olur:

Error: template deneme.algoritma(T) if (T.sizeof == 8) cannot deduce template function from argument types !()(int)

Şimdi güzel olan, o hatanın 'main' içindeki kullanıma işaret etmesidir. Yani artık hata, şablonun içindeki bir kodda değil, o şablonu uygunsuz bir türle çağıran programcıdadır. Güzel: yani şablon kısıtlamaları bu işe yarıyor.

Şimdi asıl konuya dönelim ve yukarıdaki uçan nesne kullanan 'kullan' şablonu için bir kısıtlama yazmaya çalışalım. O şablonun yalnızca o dört üye işlevi sunan türlerle kullanılabildiğini, derleyiciye (ve programcıya) güzel bir dille anlatmak istiyoruz.

Şu çalışır:

void kullan(T)(T nesne)
   if (is (typeof(T.hazırlan)) &&
       is (typeof(T.uç)) &&
       is (typeof(T.kon)) &&
       is (typeof(T.anlat)))
{
   // ...
}

O kısıtlama, derleme hatasının istediğimizi gibi, 'main''deki kullanıma işaret etmesini sağlar:

   auto taklacı = new Güvercin;
   kullan(taklacı);   // <-- DERLEME HATASI

Tamam: amacımıza ulaştık. Ancak, kısıtlamalar bazen anlaşılır olamayabilirler ve buradakinden daha karmaşık olabilirler. Bazen, yukarıdaki kısıtlama yerine, tam olarak derdimizi anlatan bir söz kullanmak daha kullanışlı olabilir:

void kullan(T)(T nesne)
   if (uçabilir_mi!T)
{
   // ...
}

İşte, yazının başında verdiğim 'uçabilir_mi' şablonu o işe yarar. Artık bir bakışta bu şablonun uçabilen türlerle kullanılması gerektiği anlaşılır.

En sonunda, bütün bu yazının amacına geldim: 'uçabilir_mi''nin nasıl çalıştığını anlatmak istiyordum. Yararlanılan D olanaklarını sıralayacağım:

Tek tanım içeren 'template' blokları: Eğer şablon bloğunda tek tanım varsa, ve o tanımın ismi şablonun ismi ile aynıysa; o şablon doğrudan içindeki tanım anlamına gelir. Kötü bir örnek olsa da, bunu şu şablonda görebiliriz:

import std.stdio;

template birDeğişken(T)
{
   T birDeğişken;
}

void main()
{
   birDeğişken!int = 42;
   birDeğişken!double = 1.2;

   writeln(birDeğişken!int, ' ', birDeğişken!double);
}

O kodda örneğin 'birDeğişken!int', 'int birDeğişken' ile aynı şeydir. (Evet kötü bir örnek, ama bu kadarı yeterli...)

İşte 'uçabilir_mi' şablonunun içindeki aynı isimdeki 'bool', o şablonun bir 'bool' olarak kullanılabilmesini sağlar.

Şablon kısıtlaması: Şablonun içinde tanımlanan 'uçabilir_mi' isimli 'bool''un değeri ile belirleniyor

'is' ifadesi: Buradaki 'is' ifadesi, kendisine verilen türün geçerli bir tür olup olmadığına göre 'true' veya 'false' üretiyor

'typeof' ifadesi: 'typeof', kendisine verilen ifadeyi hiçbir zaman işletmez, ama onun türünü üretir. Benim şimdiye kadar bilmediğim bir özelliği daha varmış: 'typeof''a aslında tamamen geçersiz veya yanlış kodlar da verilebiliyormuş:

   writeln(is(typeof(var_olmayan_bir_işlev())));

Yukarıdaki satır derleme hatasına neden olmaz ve çıkışa 'false' yazdırır. Bunun nedeni, 'typeof''un içindeki kodun derlenemeyecek olduğu için geçersiz bir sonuç üretmesidir. 'is' de kendisine geçersiz tür geldiğinde 'false' üretir

İsimsiz kapama: 'uçabilir_mi' şablonu içindeki ' { ... }', bir isimsiz kapama tanımlar. Ondan hemen sonra yazılan '()' parantezleri, o isimsiz kapamayı hemen oracıkta işletir. 'typeof' da o işletmenin sonucunun türünü üretir. (Tabii aslında o kapama, 'typeof' içinde olduğu için hiç işletilmez.)

Sonuç: Eğer o kapama, şablon parametresi olan T için derlenebiliyorsa,

  • 'typeof' geçerli bir tür üretir ('void' de geçerli bir türdür)

  • geçerli bir tür aldığı için, 'is''in değeri 'true' olur

  • o 'true', 'uçabilir_mi' isimli 'bool''u ilkler

  • şablonun ismi ile içindeki 'bool' aynı olduklarından, 'if (uçabilir_mi!T)' gibi bir kısıtlama yazılabilir

Bitti. :)

Ali

Not: Aslında 'uçabilir_mi' diye bir çözüm kullanmak yerine, 'UçanNesne' isminde bir arayüz tanımlanabilir ve 'kullan''ın işlev parametresi olarak onu alması da sağlanabilir:

interface UçanNesne
{
   void hazırlan();
   void uç(int);
   void kon();
   void anlat();
}

void kullan(UçanNesne nesne)
{
   // ...
}

Böylece, yalnızca o arayüzden türeyen türler kullanılabilir. Zaten arayüzlerin amacı da budur. Yani o da olur.

Arayüzlü çözüm, sınıf sıradüzeni kullanan bir çözümdür. Sınıflar, çalışma zamanı çokşekilliği getirirler ve bu konuda çok yararlıdır.

Şablonlar ise derleme zamanı çokşekilliği (compile time polymorphism) olanaklarıdır.

Bunlar iki farklı programlama yöntemidir... Ama artık iyice bu yazının dışına çıkıyor... :)

--
[ Bu gönderi, http://ddili.org/forum'dan dönüştürülmüştür. ]