D'nin Saflık Kavramı
Yazar: David Nadlinger
Çevirmen: Ali Çehreli
Çeviri Tarihi: Ekim 2013
İngilizcesi: Purity in D
Programlama dillerinin tasarımları tartışmaya açık konulardır. Buna rağmen, hem donanımda yaşanan gelişmelerin hem de belirli bir dilde yazılmış olan programların bakımlarının önemli konular olmaları nedeniyle daha önceden fonksiyonel programlamada ortaya çıkmış olan kavramlar yeni dillerde tekrardan keşfedilmektedirler. D programlama dili de fonksiyonel saflık konusuna kendi yaklaşımını getirir. Bu yazı D'nin pure
(saf) anahtar sözcüğünü tanıtmakta ve onun diğer olanaklarla nasıl etkileştiğini göstermektedir.
Saflık kavramı hem programcının hem de derleyicinin kaynak kodu anlama konusunda yararlandıkları güçlü bir araçtır. Bu olanağın yararlarına ve asıl kullanımlarına geçmeden önce pure
anahtar sözcüğünün D'de tam olarak ne anlama geldiğine bakalım. Eğer bu kavramı başka dillerden tanıyorsanız lütfen bir süreliğine hiç duymamış gibi davranın. Bu kavramın D'deki gerçekleştirmesindeki küçük farkların büyük etkileri olduğunu göreceksiniz.
pure
, bir işlevi çağıran ile o işlev arasında bir sözleşme belirleyen bir niteliktir. Saf bir işlev değişebilen (mutable) evrensel değişkenleri kullanamaz. Burada "evrensel" ile kastedilen, işlevin parametreleri dışındaki tüm değerlerdir (işlevin parametreleri de iş parçacıkları arasında paylaşılabilen (shared) veriler olamazlar). Ek olarak, burada "kullanamaz" sözü ile kastedilen, o değişkenlere okumak için de yazmak için de erişilemediğidir. Bunun tersi olarak, pure
olarak işaretlenmemiş olan bir işlev de saf değil (impure) olarak adlandırılır.
Yukarıdaki tanımın anlamı, saf bir işlevin aynı parametre değerlerine (argument) karşılık olarak hep aynı yan etkiyi üreteceği ve aynı değeri döndüreceğidir. Dolayısıyla, saf bir işlev saf olmayan işlevleri çağıramaz ve klasik anlamda giriş ve çıkış işlemleri uygulayamaz.
Saflık kavramının fazla kısıtlayıcı olmasını engellemek adına, aslında izin verilmemesi gereken bazı yan etkiler yine de mümkün bırakılmıştır (isterseniz bu ayrıntıları atlayabilirsiniz):
- Programın sonlanması: D gibi bir sistem dilinde programın hemen sonlanabilmesi her zaman için olasıdır. Bunun önüne geçmek olanaksız olduğundan D'nin saflık kavramı buna açıkça izin verir.
- Kesirli sayı işlemleri: x86 işlemcilerinde (ve belki başka işlemcilerde de) kesirli sayı hesapları evrensel bayrakların değerlerine bağlıdır. Bir işlev
x + y
veyacast(int)x
kadar masum bile olsa tek x87/SSE kesirli sayı ifadesi içerse, o işlevin etkisi veya hata atıp atmayacağı programın evrensel durumuna bağlı demektir.1 Dolayısıyla, saflık kavramının en katı tanımında tek kesirli sayı işlemine bile izin verilmemesi gerekir. Böyle bir karar fazla kısıtlayıcı olacağından, D'de saf işlevlerin kesirli işlem bayraklarını okumalarına ve onlara yazmalarına izin verilir. (Buna rağmen, bu gibi işlevlerin işlemlerinin sonunda o bayrakları tekrar eski değerlerine getirecekleri beklenir.) - Çöp toplayıcıdan bellek ayırmak: İlk bakışta gözden kaçabilse de, bellek ayırmak bile (örneğin
malloc
ile) programın evrensel durumu ile ilgili bir işlemdir çünkü sistemde ne kadar boş bellek bulunduğu evrensel bilgisinden yararlanmak zorundadır. Buna rağmen, bellek ayıramıyor olmak çok sayıda işlem için ciddi derecede kısıtlayıcı bir durum olurdu. Ancak, D'de çöp toplayıcıdan (GC)new
ile bellek ayrılamadığı zaman zatenError
'dan türemiş olan bir hata atılır. Dolayısıyla, saf işlevler de tür sisteminin (type system) verdiği garantileri ihlal etmedennew
işlecini çağırabilirler. (Not: Aslında program yığıtının (stack) kullanılması bile saflığı bozabilir çünkü yığıtın belirli bir andaki durumuna göre her işlev yığıt taşmasına (stack overflow) neden olabilir.)
Not: D'de üstesinden gelinemeyen hata türleri Error
sınıfından türerler. Bu tür hatalar @safe
2 olmayan kodlar tarafından yakalanabilseler de Error
hatalarının yakalanmalarından sonra tür sistemi artık programın işleyişi ve mutlak değişmezleri konusunda hiçbir garanti veremez.
Referans saydamlığı (referential transparency)
Fonksiyonel programlama dili dünyasında çok yaygın olarak geçen parametrelerin değişmezliği (immutability) kavramının yukarıdaki tanımda geçmiyor olması size şaşırtıcı gelmiş olabilir. D'de saf işlevler parametrelerinde değişiklik yapabilirler. Örneğin, aşağıdaki kod D'de yasaldır:
int okuVeArttır(ref int x) pure { return x++; }
Programlama dilleri açısından bakıldığında saflık kavramı normalde referans saydamlığını da içerir. Bunun anlamı, saf bir işlev çağrısı yerine o çağrının dönüş değerinin yerleştirilmesi sonucunda programın davranışında hiçbir değişiklik olmayacağıdır. Bu, D'nin varsayılan davranışı değildir. Örneğin, aşağıdaki kod
int değer = 1; auto sonuç = okuVeArttır(değer) * okuVeArttır(değer); // assert(değer == 3 && sonuç == 2);
okuVeArttır
'ın tek kere çağrılması ile aynı sonucu doğurmaz:
int değer = 1; auto geçici = okuVeArttır(değer); auto sonuç = geçici * geçici; // assert(değer == 2 && sonuç == 1);
Bir sonraki başlıkta da göreceğimiz gibi, bu aslında emirli dillerde çok aranan bir özelliktir. Buna rağmen, saflık kavramının klasik tanımının verdiği garantiler ve bunun getirdiği yararlar da göz ardı edilmemelidir. Bu konuda D'nin tür sisteminin başka bir özelliğinden yararlanılır: Veriye erişimin geçişli olarak const
olarak işaretlenebilmesi veya verinin en başından immutable
olarak tanımlanabilmesi3. Bu konuyu biraz daha yakından incelemek için aşağıdaki üç işlev bildirimine bakalım:
int a(int[] val) pure; int b(const int[] val) pure; int c(immutable int[] val) pure;
Parametresi değişebilen türden olan a
'nın durumu okuVeArttır
'ınkinin aynısıdır (int[]
türü bir gösterge ve bir uzunluktan oluşan ve belleğin bir bölgesine erişim sağlamaya yarayan bir dinamik dizidir). b
ve c
ise çok daha iyi durumdadırlar: Saf olduklarından bu işlevlerin programın evrensel durumunu okuyamadıklarını ve evrensel durumunda değişiklik yapamadıklarını biliyoruz. Dolayısıyla, b
ve c
işlevlerinin kelimenin yaygın anlamı ile yan etkileri olmadığını ve bu yüzden bu işlevlere yapılan çağrıların referans saydamlığına sahip olduklarını söyleyebiliriz.
Peki, b
ile c
arasındaki bir farklılıktan söz edilebilir mi? Saflık kavramı açısından hayır, çünkü const
ve immutable
işlevin parametre üzerindeki hakları konusunda aynı anlama gelir. (immutable
ayrıca verinin kesinlikle değişmeyeceği garantisini de verir ama işlev saf olduğundan bu parametreye erişim sağlayan referanslar zaten işlevden dışarıya sızdırılamazlar (dönüş değeri dışında).)
Yine de, işlevi çağıranı ilgilendiren ince ama önemli bir farktan söz edilebilir: Bu fark, parametre değerinin gerçekte yalnızca const
mı yoksa immutable
mı olduğuna bağlıdır. Bunun nedeni, const
'ın hem değişebilen hem immutable
değerlere erişim sağlayabilmesine karşın immutable
'ın yalnızca immutable
olan veya immutable
'a dönüşebilen değerlere erişim sağlayabilmesidir. Dolayısıyla, aşağıda yazılanlar immutable
bir dizi ile çağrılan durumda hem b
için hem de c
için aynen geçerlidir.
Örnek olarak, ifade değerlerini hatırlamaya (memoization) veya alt ifadelerin azaltılmalarına dayanan eniyileştirme yöntemlerini düşünelim. pure
bir işleve aynı immutable
değerlerle yapılan birden fazla çağrının teke indirilebilmesi için o değerlerin yalnızca kimliklerine bakılması yeterlidir. (Örneğin, yalnızca o değerlerin adreslerine bakmak gibi bir kaç basit karşılaştırma yaparak.) Öte yandan, const
olan bir parametre değerinin değişebilen referanslar içerdiği durumda o referans iki çağrı arasında başka kodlar tarafından değiştirilebileceğinden, o referansların çalışma zamanında derinlemesine incelenmeleri veya derleyicinin program akışını derleme zamanında anlayabilmesi gerekir.
Aynı durum koşut işlemleri de etkiler: pure
bir işlevin parametre değerlerinin referans içermediği veya yalnızca immutable
referans içerdiği durumlarda işlemlerin koşut olarak işletilmelerinde bir sakınca yoktur çünkü o işlev programın davranışında belirsizliklere neden olabilecek yan etkiler oluşturamaz; ek olarak, parametre değerlerine erişim konusunda yarış (data race) halinde de olunamaz. Ne var ki, const
parametreleri inceleyerek aynı tür sonuçlara varılamayabilir çünkü aynı değerlere değişebilen çeşitten erişimi olan kodlar onları değiştirebilirler.
Dönüş türünün referans içermesi
Parametrelerinin değişebilme açısından farklılıklar göstermelerine karşın, yukarıdaki örneklerdeki a
, b
, ve c
işlevlerinin üçünün de dönüş türü bu gibi örneklerde sık kullanılan int
idi. Peki, saf bir işlevin referans içeren bir tür döndürdüğü durumlarda neler önemli olabilir?
İlk önemli konu, referans saydamlığı göze alınırken eşitlik karşılaştırmalarında yararlanılan adreslerdir. Genelde fonksiyonel dillerde bir verinin belleğin hangi noktasında bulunduğunun önemi ya hiç yoktur ya da çok azdır. Bir sistem dili olan D bu kavramı da kullanıma sunar. Örneğin, bir dizi oluşturan ve o diziyi ilk adet
adet asal sayı ile dolduran ulong[] asallar(uint adet) pure
işlevine bakalım. asallar
aynı adet
değeri ile her çağrıldığında aynı sonucu üretiyor olsa da, o sonuçları içeren diziler her seferinde farklı adreslerde bulunacaklardır. Dolayısıyla, dönüş değerlerinde referans içeren işlevlerin referans saydamlığı göze alınırken bit düzeyindeki eşitlik kavramı (is
) değil, mantıksal anlamdaki eşitlik kavramı (==
) önemlidir.
Referans saydamlığındaki ikinci önemli konu ise dönüş türünün değişebilen referanslar içerip içermediğidir. asallar
işlevini kullanan aşağıdaki koda bakalım:
auto p = asallar(42); auto q = asallar(42); p[] *= 2;
asallar
'a yapılan yukarıdaki ikinci çağrı yerine q = p
işleminin gelemeyeceği açıktır çünkü o durumda q
da p
ile aynı bellek bölgesine erişim sağlayacağından çarpma işleminden sonra o da sonuçların iki katlarına sahip olmaya başlayacaktır. Genel açıdan bakıldığında, dönüş türünde değişebilen referanslar içeren saf işlev çağrılarının referans saydamlığı taşıdığı söylenemez; buna rağmen, bazı çağrılar çağıran taraftaki kullanımlara bağlı olarak yine de eniyileştirilebilirler.
Yarı saflık ve getirdiği garantiler
D'nin pure
anahtar sözcüğünün ilk tasarımı çok daha sıkı kurallara bağlıydı. Dil, o zamanlarda bu yazının başında verilen tanıma uyan tek saflık kavramı içeriyor idiyse de, sonuçta kabul edilen ve daha gevşek kurallar getiren bugünkü saflık kavramının tartışmaları sırasında iki terim ortaya çıkmıştı: Yukarıdaki okuVeArttır
ve a
işlevlerinde de görüldüğü gibi, değişebilen parametrelere sahip işlevleri tanımlayan yarı saf (weakly pure), ve b
ve c
işlevlerinde görüldüğü gibi, yan etkileri bulunmayan işlevleri tanımlayan tam saf (strongly pure). Ancak, bu terimler konusunda tam bir anlaşma bulunmamakta ve bunların forumlardaki kullanımları sürekli olarak karışıklıklara neden olmaktadır; bu terimleri saflık kavramı tasarımında ilk ortaya atan Don Clugston bile artık kullanılmamalarını istemektedir.
Buna rağmen, belki de parametre ve dönüş türlerine bağlı olarak verilen garantilerin farklılıkları nedeniyle bu terimler hâlâ kullanılmaktadır. Çok basit olmalarına rağmen belki de kurallarına yabancı kalındığından D'deki saflık kavramı tam olarak anlaşılamamaktadır. Peki, saf işlevlerin parametrelerini değiştirmelerine neden izin verilmektedir?
D'deki saflık kavramının arkasındaki asıl güç, kuralları böyle yumuşatmanın aslında daha fazla sayıda işlevin tam saf olabilmelerini sağlamasıdır. Bunu göstermek için id Software'den tanınan John Carmack'ın Functional Programming in C++ başlıklı #AltDevBlogADay yazısından bir bölüm aktarayım. Yazı, fonksiyonel programlama ilkelerinin C++'a uygulanmasının yararlarını gösterir:
ÜçgenÇiz
gibi bir işlev parametre olarak bir çerçeve ara belleği (framebuffer) alır ve sonuç olarak içine üçgen çizilmiş olan yepyeni bir çerçeve ara belleği döndürür. Bunu yapmayın. — http://www.altdevblogaday.com/2012/04/26/functional-programming-in-c/
Yukarıdaki görüş doğrudur; çerçeve ara belleğinin her üçgen çizimi için tekrar kopyalanmasının iyi bir fikir olmadığı açıktır. Buna rağmen, üçgen çizen saf bir işlev D'de hızdan ödün vermeden de gerçekleştirilebilir! Böyle bir işlevin bildirimi aşağıdaki gibi olabilir:4
alias Renk = ubyte[4]; struct Köşe { float[3] konum; /* ... */ } alias Üçgen = Köşe[3]; void üçgenÇiz(Renk[] çerçeve, const ref Üçgen üçgen) pure;
Bu kadarıyla iyi: Yukarıdaki alıntıda da belirtildiği gibi, üçgenÇiz
'in referans saydamlığı taşıdığını söyleyemiyoruz çünkü çerçeve ara belleğine yazmak zorundadır. Yine de, pure
belirteci sayesinde bu işlevin programın gizli veya evrensel durumunu kullanamadığından eminiz. Dahası, saf olması nedeniyle bu işlev başka saf işlevler tarafından da çağrılabilmektedir. Küçük örneğimize devam edersek, eğer her çerçeve için yeni bir ara bellek ayrılsaydı, üçgenlerden oluşan bir görüntüyü oluşturan bir işlev aşağıdaki gibi olurdu:
Renk[] sahneÇiz( const Üçgen[] üçgenler, ushort en = 640, ushort boy = 480 ) pure { auto sahne = new Renk[en * boy]; foreach (ref üçgen; üçgenler) { üçgenÇiz(sahne, üçgen); } return sahne; }
sahneÇiz
'in parametrelerinin değişebilen referanslar içermediklerine dikkat edin – parametrelerinde değişiklik yapan üçgenÇiz
'i çağırdığı halde kendisi parametrelerinde değişiklik yapmamaktadır!
Bu her ne kadar zorlama bir örnek olsa da, emirli kod performansından ödün vermeyen D'de benzer durumlarla sık karşılaşılır (örneğin, değişebilen toplulukların saf işlevler içinde geçtikleri her durum). Bu konu saflık kavramının yukarıda anlatılan ilk tasarımından edinilen deneyimlere de uymaktadır – saflık kuralları yumuşatıldığında, aynı güçlü garantiler çelişkili bir biçimde daha çok sayıda kod için sağlanabilmektedir.
Çoğu modern kodlama standardı zaten evrensel değişkenlerden kaçınılmasını önerdiğinden, giriş çıkış işlemleri ile ilgilenmeyen çoğu D işlevi kolaylıkla pure
olarak işaretlenebilir. Bu doğru olduğuna göre neden varsayılan belirteç pure
olmamıştır ve diğer işlevlerin örneğin impure
diye işaretlenmeleri gerekmemiştir? D'nin ikinci sürümü açısından bakıldığında bunun nedeni saflık kavramının son tasarımının dilin gelişmesinin sonlarına rastlaması ve böyle bir değişikliğin halihazırda yazılmış olan kodları etkileme riskinin fazla yüksek olmasıdır. Yine de bu, gelecekteki başka diller ve belki de D'nin bir sonraki ana sürümü açısından dikkate alınacak bir fikirdir.
Şablonlar ve saflık
Bu noktaya kadar saflık tasarımını başka olanaklardan bağımsız olarak ele aldık. Bundan sonraki başlıklarda öncelikle işlev şablonları olmak üzere saflığın diğer dil olanaklarıyla etkileşimlerini göreceğiz.
Bir işlev şablonunun tür parametrelerinin belirli türler için her kullanımı, sıradan bir işlevin eşdeğeridir. Bu yüzden saflık kavramı onlar için de yukarıda anlatıldığı gibidir. Buna rağmen, belirli bir şablonun saf olup olamadığı o şablonun kullanıldığı türlere bağlı olabildiğinden şablonların durumu biraz daha karışıktır.
Bunun bir örneği olarak bir aralık5 (range) alan ve o aralıktaki elemanlardan oluşan bir dizi döndüren bir işleve bakalım (bu işlev std.array
modülünde zaten bulunur ve oradaki gerçekleştirmesi çok daha kullanışlıdır). Böyle bir işlev aşağıdaki gibi yazılabilir:
auto array(R)(R r) if (isInputRange!R) { ElementType!R[] sonuç; while (!r.empty) { sonuç ~= r.front; r.popFront(); } return sonuç; }
Yukarıdaki işlev, bir aralıktaki elemanları bir dizi olarak döndüren std.array.array
'in fazla hızlı olmayan bir gerçekleştirmesidir. Bu işlev pure
olabilir mi?
Bu işlevin nasıl işlediğini tahmin etmek güç olmasa gerek – aralığın başındaki eleman aralıktan çıkartılıyor ve dizinin sonuna ekleniyor, ve bu işlem aralıkta eleman kalmayana kadar sürdürülüyor. Soru: Bu işlev pure
yapılabilir mi? R
türü map
veya filter
gibi işlevlerin dönüş türü olduğunda bu kodun saf kodlar tarafından çağrılamaması için bir neden yoktur. Öte yandan, eğer R
örneğin girişten satır okuma gibi bir işlemi de sarmalıyorsa r.empty
, r.front
, ve r.popFront
'un hepsinin birden pure
olmaları olanaksızdır. Dolayısıyla, array
pure
olarak işaretlense, böyle aralıklarla çalışamaz. Peki, ne yapılabilir?
Buna bir çözüm olarak pure
belirtecini R
'ye bağlı olarak koymayı sağlayan bir D söz dizimi değişikliği önerilmişti. Bu çözüm önerisi dile getireceği karmaşıklık ve neden olacağı aşırı kod tekrarı nedeniyle benimsenmedi. Sonuçta kabul edilen çözüm oldukça basittir: D'de şablon kodları nasıl olsa derleyici tarafından görülmek zorunda olduklarından şablonların saflığı derleyici tarafından otomatik olarak belirlenebilir (aynısı nothrow
ve başka belirteçler için de geçerlidir).
Bunun anlamı, yukarıdaki örnekteki array
'in buna izin veren aralık türleri için saf işlevler tarafından çağrılabilmesi, başka türler için ise çağrılamamasıdır. Saflığın şablon parametrelerine bağlı olmadığı durumda işlev şablonları en azından belgeleme açısından yine de açıkça pure
olarak işaretlenebilir.
Saf üye işlevler
Doğal olarak, yapı ve sınıf üye işlevleri de saf olabilir. Normal işlevlerin saflık kuralları bir farkla onlar için de geçerlidir: Saflık kuralları üye işlevlerin gizli this
parametresini de kapsar. Bunun anlamı, saf işlevlerin üye değişkenlere de erişebilecekleri ve onları da değiştirebilecekleridir.
class Foo { int getBar() const pure { return bar; } void setBar(int bar) pure { this.bar = bar; } private int bar; }
Saf işlevler üye değişkenlere erişebilirler. (Not: Normalde getter/setter işlevleri yerine D'de nitelik işlevlerinden (property) yararlanılır. )
Bir üye işlev const
veya immutable
olarak işaretlendiğinde de o belirteç aslında this
parametresine uygulanır. Dolayısıyla, yukarıda değişebilme konusunda anlatılanlar burada da geçerlidir.
Saflık, türeme konusunda da beklenebileceği gibidir: Genel olarak, bir alt sınıfın beklentileri üst sınıfınkilerden daha azdır; verdiği garantiler ise daha fazladır (dönüş türlerinin ortakdeğişkeliğinde (covariance) görüldüğü gibi). Dolayısıyla, saf olmayan bir işlevin alt sınıftaki tanımı saf olabilir ama bunun tersi doğru değildir. Bir kolaylık olarak da, saf olan bir üst sınıf işlevinin alt sınıftaki tanımının açıkça pure
olarak işaretlenmesi gerekmez; böyle bir işlev otomatik olarak saftır (C++'taki virtual
gibi). Walter Bright'ın bu konuda bir blog yazısı vardır.
pure
ve immutable
– bir kere daha
const
ve immutable
belirteçlerinin referans saydamlığına etkilerini yukarıda gördük. pure
'un getirdiği garantiler bazı durumlarda bazı ek çıkarımlar da sağlar. Bunun tür sisteminin bir parçası da olan belirgin bir örneği, pure
işlevlerin dönüş türlerinin bazı durumlarda güvenle immutable
olarak kullanılabilmeleridir. Örnek olarak yine yukarıdaki ulong[] asallar(uint adet) pure
işlevine bakalım. Aşağıdaki kodun nasıl olup da derlenebildiği ilk bakışta açık olmayabilir:
immutable ulong[] a = asallar(5);
immutable
bir verinin başka hiçbir değişebilen referansı olamadığını biliyoruz. Buna rağmen, asallar
'ın döndürdüğü ve değişebilen değerlerden oluşan dizi immutable
olarak tanımlanabilmektedir. Peki, yukarıdaki kodun derlenebilmesinin nedeni nedir? Döndürdüğü değerleri değiştirebilecek olan başka referansların bulunamayacağı, bir işlevin pure
olmasının sonucudur: Böyle bir işlev değişebilen referanslar içeren parametrelere sahip değildir ve değişebilen evrensel değişkenlere erişemez. Dolayısıyla, her ne kadar değişebilen değerlerden oluşan bir referans döndürüyor olsa da o işlevi çağıranlar o değerlerin değişemeyeceklerinden emindirler.
Her ne kadar fazla önemsiz bir ayrıntıymış gibi görünse de bu aslında şaşırtıcı derecede kullanışlı bir olanaktır çünkü hem işlevlerin fonksiyonel programlamada olduğu gibi değişmez veriler kullanabilmelerini, hem de verinin hız nedeniyle olduğu yerde değiştirildiği geleneksel kodlarda gereksiz veri kopyalarının önüne geçilmesini sağlamaktadır.
Peki, kaçış kapısı nerede?
Saf bir işlevin kullandığı bütün kodların da saf olmaları gerektiği gerçeğinden yola çıkıldığında saflığın bulaşıcı (viral) olduğu görülür. D'nin saflık kuralları her ne kadar bu konuda kolaylıklar getiriyor olsalar da bazen saf olmayan kodların saf kodlar tarafından çağrılmaları gerekebilir.
Bunun bir örneği, halihazırda yazılmış olan (legacy) kodlardır. Örneğin, saflığın gereklerini yerine getirdiği halde pure
olarak işaretlenmemiş olan bir C işlevi bulunabilir. Tür sisteminin kendi başına çıkarsayamadığı başka durumlarda da olduğu gibi, bu durumun üstesinden gelmenin çaresi de cast
kullanmaktır. İşlevin adresi alınır, tür dönüşümü ile pure
belirteci eklenir, ve işlev o adres yoluyla çağrılır (tür sistemini yanıltabildiğinden @safe
kodlar içerisinde bu mümkün değildir). Belirli bir kod içinde böyle numaralara ihtiyacın yeterince fazla olduğu durumlarda safOlduğunuVarsay
(assumePure
) gibi bir şablondan yararlanılabilir.
Saflık bazen kısa süreliğine de olsa ayak bağı olabilir: Hata ayıklama amacıyla ekrana mesaj yazdırmak için veya istatistiksel amaçla evrensel bir sayacı arttırmak için bir işleve saf olmayan kodlar eklenmiş olabilir. Saf olmayan böyle bir kodun zincirleme çağrılmış olan pure
işlevlerin en içtekine eklenmesinin gerekmesi ama bunun tür sistemi tarafından engellenmesi büyük bir sıkıntı kaynağı olurdu. Her ne kadar bazılarınca küçümsenen bir davranış olsa da bu tür kodlar gerçekte oldukça kullanışlıdırlar.
Saflığın hata ayıklama amacıyla geçici olarak etkisizleştirilmesi ihtiyacına rağmen başlangıçta D'de bu kullanıma yönelik hiçbir olanak bulunmuyordu. Sonunda özel bir kural olarak debug
blokları içindeki saf olmayan kodların saf kodlar arasında bulunmalarına izin verildi. Bu kullanışlı olanağın etkinleştirilmesi için derleyiciye -debug
komut satırı seçeneğinin verilmesi gerekir.
Sonuç
Başlangıçtaki görüşü tekrarlarsak, saflık kavramının önemi, işlevlerin gizli durum kullanmadıklarının tür sistemi tarafından garanti edilmesidir. D'nin pure
anahtar sözcüğünün başka dillerdekilerden daha az kısıtlama getirdiğini gördük. Buna rağmen, const
ve immutable
belirteçleri yardımıyla aynı garantilerin sağlanabildiklerini ve pure
'un D'nin başka olanakları ile etkileşimlerinin doğal olabildiğini ve belki de en önemlisi, emirli programlamaya izin verilebildiğini gördük.
Peki, bu konuda daha fazla bilgi için başka kaynaklar nelerdir? Aslında pure
anahtar sözcüğü ile ilgili kurallar oldukça kısadır. Bu kurallar için dil referansının dlang.org'daki Functions bölümüne bakabilirsiniz. Don Clugston'ın başlatmış olduğu ve son değişikleri de getirmiş olan tartışma da pure
'un gelişimini görmek açısından oldukça ilginçtir. Burada anlatılan kavramların tasarımları ve gerçekleştirmeleri ile ilgili soruları da D dili forumlarında sormak isteyebilirsiniz.
Bu yazıyı beğendiyseniz bana fikrinizi bildirmenizi, yazıyı Twitter'da paylaşmanızı, ve yazının Hacker News'deki ve Reddit'teki tartışmalarına katılmanızı isterim. D hakkında başka yazılarım da var.
Notlar
- Bunun etkileri ilk akla geldiğinden çok daha ciddi ve şaşırtıcı olabilir: Bir çok Windows yazıcı sürücüsünün yazdırma işlemi sırasında değiştirdikleri FPU bayraklarını tekrar eski değerlerine çevirmedikleri olurdu. Bir belge yazdırıldıktan sonra bir çok programın çökmesi bunun yüzündendi – bu, yalnızca müşteri bilgisayarında oluşan çökme hatalarının güzel bir örneğidir.
- D kodları dilin bazen SafeD olarak adlandırılan bir alt kümesine kısıtlanabilir. Bu olanak üç anahtar sözcük sayesinde her işlev için teker teker ayarlanabilir:
@safe
olarak işaretlenen kod bellek hataları içeremez; dolayısıyla, bu gibi işlevlerde örneğin gösterge aritmetiği ve C'deki gibi bellek yönetimi kullanılamaz.@system
olarak işaretlenmiş olan kod bunun tersidir – bu gibi işlevlerde inline assembly de dahil olmak üzere dilin bütün olanakları kullanılabilir. Son olarak,@trusted
belirteci bu iki kavram arasında bir köprü kurar; programcının araştırarak güvenli olduğuna karar verdiği halde aslında güvenli olarak işaretlenmemiş olan kodlara arayüz oluşturur.void*
türünden bir C arayüz işlevini sarmalayan bir D işlevi bunun bir örneği olarak görülebilir. - C++'ın tersine, D'de
const
veimmutable
geçişlidir. Birconst
referans yoluyla erişilen her değer otomatik olarakconst
'tır. Örneğin, aşağıdaki kodu ele alalım: - Bu örnek yalnızca açıklayıcı olduğu için seçilmiştir; yoksa, bu işlev herhalde ancak çok basit bir görüntüleyicide kullanılabilir. Saflığın bu örneğe bir yarar katıp katmadığı bir yana, bu işlev gerçek bir grafik arayüzü üzerinde gerçekleştirilmiş olsa, GPU'nun durumunun saflığının nasıl sağlanacağı konusu da düşünülmelidir.
- C++'ın erişicilerinin (iterator) gösterge genellemeleri olmalarına benzer biçimde, D aralıkları da topluluk kavramının genellemeleridir. En basit aralık üç temel işlev sunar:
empty
,front
, vepopFront
. Bu arayüzün kullanıcıları verinin perde arkasında nasıl saklandığından habersizdirler – örneğin, veri bir bellek bölgesinden, ağ üzerinden, standart girişten, vs. geliyor olabilir. Aralıklar algoritmaların kullanabilecekleri basit ama güçlü bir soyutlama sunarlar.
struct Foo { int bar; int* baz; }; void fun(const Foo* foo);
C++'ta fun
foo
'nun kendisini değiştiremez; örneğin, foo->bar
'a yeni bir değer atayamaz. Buna rağmen, foo->baz
'ın gösterdiği değeri değiştirmesi yasaldır – buna sığ const (shallow const) da denir. Öte yandan, D'de const
derindir; fun
içinde foo.baz
, const int
gösteren const
bir göstergedir ve bu yüzden *foo.baz
'da da değişiklik yapılamaz. Aynı kural immutable
için de geçerlidir; ek olarak, immutable
olan veriyi değiştirebilecek başka referans bulunmadığı da garanti edilmiştir. Yani, veriyi fun
değiştiremediği gibi, başka hiçbir kod da değiştiremez (immutable
değerlerin ROM gibi bir bellek bölgesinde duruyor olduklarını düşünebiliriz). immutable
otomatik olarak const
'tır da.