11 Ocak 2012 Çarşamba

Clojure, Java Platformunun Fonksiyonel Programlama Dili

Bu yazımızda, daha önceden fonksiyonel programlamaya aşık olup da JSM aleminde aşklarının karşılıksız kaldığını düşünenlerin yanıldığını anlatmaya çalışacağız: JSM'nin fonksiyonel programlama dillerinden Clojure'a (okunuş: Klojur) göz atacağız. Yeni bir programlama dili öğrenmek fikrinden korkanlarınızın içi rahat olsun, Common Lisp, Scheme gibi dilleri bilenlerde karşı konulmaz bir déjà vu hissi uyandıracak Lisp ailesinin bu genç üyesinin sıfırdan öğrenilmesi, anılan ailedeki dillerin yalın sözdizimi ve az sayıdaki kuralı nedeniyle pek fazla zorlayıcı olmayacak. Yeter ki, programlamanın aslında eğlenceli bir şey olabileceğine ve bir problemin birden çok farklı şekilde çözülebileceğine inanın.


Peki Ama, Neden Clojure?


Çok yerinde bir soru. Zira, Clojure arkasında büyük bir şirket veya programcı topluluğunun bulunduğu bir programlama dili değil. Ancak; Java, Scala🔎, Jython, JRuby, BeanShell🔎, Groovy, vd. gibi, Clojure da bir JSM dili: Java programlama dilinin kullanımına hazır olan tüm kitaplıklar Clojure kaynak kodu içinden de kullanılabilir; Clojure kaynak kodu istenecek olursa sınıf dosyasına derlenerek Java platformunun parçası haline getirilebilir. Ayrıca, bazı diğer özelliklerinin Clojure'u önümüzdeki yıllarda daha çekici kılacağını söylemek falcılık olmayacaktır. Dolayısıyla, Clojure'u elinizin tersiyle itmeden önce biraz daha düşünmenizde yarar var.

Clojure'un Lambda Hesaplama Kuramı (İng., Lambda Calculus) üzerine inşa edilmiş bir fonksiyonel programlama dili olduğunu söyleyerek başlayalım. Süslü kısmı aradan çıkararak ifade edecek olursak, problem çözümünün matematikteki karşılıklarına benzer bir şekilde ele alınan ve fonksiyon adı verilen altyordamlar yoluyla sağlandığını söyleyebiliriz; doğal olarak, problemlerin fonksiyonların bileştirilmesiyle çözüldüğü bu paradigmada birimsellik aracı olarak fonksiyonlar kullanılır. Birinci sınıf vatandaş olan fonksiyonlar, diğer fonksiyonlara argüman olarak geçirilip, fonksiyonlardan sonuç olarak döndürülebilir ve herhangi bir bileşke türlü değerin bileşeni olarak tutulabilirler. Yani, C'de fonksiyon göstericileri, Java'da arayüzler vasıtasıyla zahmetli bir şekilde, kısmen elde edilebilen bir şey, Clojure'da çok daha kapsamlı bir paketin parçası olarak bedavaya gelir.1 Bu, Google tarafından geliştirilmiş olup yoğun bir biçimde kullanılan ve Apache'nin Hadoop projesi vasıtasıyla özgür yazılım dünyasına kazandırdığı MapReduce adlı bulut hesaplama çerçevesinin önümüzdeki aylar, yıllar içinde karşınıza çıktığında işinizin daha kolay olacağı anlamını taşır. Çünkü, MapReduce kullanılarak üretilecek çözümler, fonksiyonel programlama dillerinin vazgeçilmezi map, reduce ve filter gibi yüksek dereceli fonksiyonların—argüman olarak fonksiyon alan ve/veya dönüş değeri olarak fonksiyon döndüren fonksiyon—birlikte kullanımını andıracaktır. Buna Hadoop temelli yazılım çerçevelerinin gerçekleştirim dilinin Java olması da eklendiğinde, çok çekirdekli hesaplamaya ince ayarlı bir JSM dili olan Clojure'un farklılaştırıcı özelliği takdir edilecektir.

Gelelim Lisp ailesi dillerinin belki de ustaları için en çekici olan özelliğine: bu dillerde kaynak kod ve veri için aynı gösterim kullanılır. Bir diğer deyişle, Clojure'un da üyesi olduğu Lisp ailesi dilleri eşgösterimlidir (İng., homoiconic). Bu, kaynak kodun veri olarak ele alınmasına olanak tanıdığı gibi, çalışmakta olan bir programın kendisine sunulan girdiyi kod olarak ele alıp işlemesine olanak tanır. Örneğin—Java kaynak kodu içinden XML dosyalarındaki kurulum bilgilerinin okunmasında olduğu gibi—yazılım sevkiyatı (İng., software deployment) sırasında güvenlik, seyir defteri güncelleme gibi müşteriye özel olarak farklı icra edilmesi istenebilecek hizmetlere dair bilgileri kurulum dosyasına Clojure programı yazar gibi yazıp bir diğer Clojure programından okuyarak işinizi görebilirsiniz. Ya da, kullanıcının girdi olarak sağladığı veriyi kod olarak ele alıp işleyerek programınızı çalışırken değiştirme şansına sahip olabilirsiniz.

Clojure'u özendirmek amacıyla öne süreceğimiz son sebep, eşgösterimlilik özelliği sayesinde diğer programlama dillerindekilerin çok ötesine geçen makro olanağıdır. Clojure makro işlemcisi, programın çalıştırılması öncesinde kaynak kodu o anda tanımlı bulunan makrolara uygun bir biçimde dönüştürür ve programcıların gördüğü ile yorumlayıcının işlediği kaynak kodun birbirlerinden farklılaşmasını sağlar. Böylece, programlama dilinde bulunmayan bir denetleme yapısının—mesela, koşut işlemeli bir döngü—dile kusursuz bir biçimde yamanarak pek çok programın daha kolay yazılması mümkün olacaktır. Benzer şekilde, yinelenen kod parçalarının şablon görevi gören makrolar yoluyla yazımı kolaylaştırılabilir. Programcının problem alanına yönelik yüksek düzey denetim yapıları ve kod şablonları yaratmasına olanak tanıyan bu özellik, Clojure'u (ve diğer Lisp ailesi dillerini) alana özel dil (İng., domain-specific language) gerektiren durumlarda öne çıkarmaktadır.

Kurulum


Sisteminizde bulunmadığı takdirde clojure.org/download adresinden indirebileceğiniz Clojure ortamının kurulumu, indirilen arşiv dosyasının içindeki clojure-x.y.z.jar dosyasının sınıf yolu üzerine konulması kadar basit.2 Yorumlayıcı (Clojure komut kabuğu) anılan paketin veya bu paketteki main sınıfının JSM'ye geçirilmesi ile çalıştırılabileceği gibi, evvelden kurulu bir Clojure ortamının bulunması durumunda clojure komutu ile de çalıştırılabilir.3
$ # java -cp ".:/arşiv/yolu/clojure-1.3.0.jar:$CLASSPATH" clojure.main
$ # java -cp ".:$CLASSPATH" -jar /arşiv/yolu/clojure-1.3.0.jar
$ clojure
Clojure 1.3.0
user=>
Yorumlayıcının başlaması, oku-değerlendir-bas döngüsünün (İng., read-evaluate-print loop [REPL]) başladığı anlamına gelir. Bundan sonra yapmamız gereken, Clojure dilinin sözdizimine uygun bir şeyler girip basılan sonuçları gözlemlemektir.
user=> (+ 2 3) → 5
user=> (def sayı 5)
#'user/sayı
user=> (+ 2 3 sayı) → 10
user=> (* 2 (+ 3 4)) → 14
user=> (System/exit 0) ; Komut kabuğuna Ctrl-D ile de dönülebilir
$
Oturumu inceleyerek bazı saptamalarda bulunalım. Öncelikle; Clojure kaynak kodu, yorumu ilk konumundaki simgeye göre değişen, ayraçlarla çevrili formlardan oluşur. İstenecek olursa, bir form bir diğerinin içine gömülebilir. Böyle bir durumda, en son açılan ayracın ilk kapatılması ve dıştaki formun bittiği noktada eşleştirilmemiş ayraç olmaması gerektiği unutulmamalıdır.

Formlar, deyimler, makrolar ve özel formlar olmak üzere üçe ayrılır. Deyimler, ilk konumdaki simgenin geri kalanlara uygulandığı işlemler olarak ele alınırken, makrolar kodu dönüştüren işlemciler olarak görülebilir; özel formların işleyiş mantığı duruma göre değişir. Örneğin, (+ 2 3) toplamanın 2 ve 3 argümanlarına uygulandığı işleme karşılık gelen bir deyim iken, (def sayı 5) çalışma ortamını 5 değerine sahip sayı adlı tanımlayıcının varlığı ile güncelleyen bir özel formdur.

Kod ve veri gösterimi için işleç-önde gösterimin şaşkınlığını üzerinizden attıktan sonra, bu seçimin aslında bazı artılarının olduğunu görebilirsiniz. Örneğin, bu gösterimin bir sonucu olarak ortaya çıkan tüm deyimlerin ayraç çifti ile çevrelenmesi zorunluluğu, öncelik sırası sorununu ortadan kaldırır; her bir kapatıcı ayraç eşleştiği ayracın başlattığı formun değerlendirilmesi gerektiğini bildirir. Ayrıca, kapatıcı ayraca kadar her şey argüman olarak yorumlandığı için, aynı işleç argümansız kullanılabileceği gibi, 3 veya 5 argümanlı da kullanılabilir. Mesela, (*) çarpmanın etkisiz elemanı olan 1 değerini döndürürken, (* 1 2 3 4 5) 5!, yani 120, değerini döndürür.

Son olarak, System/exit Java platformundaki System sınıfının exit adlı metoduna atıfta bulunmaktadır. Yani, oturumun son satırında, bir Java programı olan yorumlayıcıya programdan 0 değeri döndürecek şekilde çıkmak istediğimizi söylüyoruz. Genelleştirecek olursak, Sınıf adlı bir Java platformu sınıfının üye adlı static bir özelliğini kullanmak istediğimizde yapmamız gereken, deyimimizin ilk konumuna Sınıf/üye koymaktan ibarettir.

Fonksiyonlar Birer Hesaplama Nesnesidir


Mesele fonksiyonel bir programlama dilini anlatmaya çalışmaksa, yapılması gereken ilk şeylerden biri, hiç kusku yok ki, birimsellik aracı olan fonksiyonun tanımı ve kullanımından bahsetmektir. Bahis esnasında fonksiyonların matematikteki adaşlarına benzediği ve diğer türden değerlerle birlikte birinci sınıf vatandaşlar olarak kullanılabileceği akıldan çıkarılmamalıdır. Gelin, ne kastedildiğini aşağıdaki tanımlardan görelim.
user=> (def çarp (fn [a b] (* a b)))
#'user/çarp
user=> (def çarp2 #(* %1 %2))) ; çarp ile aynı
#'user/çarp2
user=> (def arg1 3)
#'user/arg1
user=> (def arg2 (read)) ; standart girdiden veri alıyor
17
#'user/arg2
user=> (çarp arg1 arg2) → 51
user=> ((fn [x y] (* x y) 4 5) → 20
user=> (#(* % %2) 6 7) →42
user=> (def çarp3 (partial çarp 3))
#'user/çarp3
user=> (çarp3 20) → 60
çarp ve çarp2'nin arg1 ve arg2 ile aynı form kullanılarak tanımlandığına dikkatinizi çekerek başlayalım. Komutsal programlama dillerinden gelenlere başta aykırı gözükebilecek bu durum aslında çok doğal: ne de olsa, ister tamsayı olsun isterse fonksiyon, dört tanımlayıcı da birer değeri simgeliyor. Değerlerden ikisi sayma kavramının örnekleri iken, diğer ikisi hesaplama kavramının örneklerini veriyor; ikisi (arg1 ve arg2) matematikte bağımsız değişken olarak ifade edilirken, diğerleri iki argümanlı bağımlı değişkenler olarak adlandırılıyor. Dolayısıyla, heseplamayı soyutlayan fonksiyon değerlerinin, 11. ve 12. satırlarda olduğu gibi, ad verilmeksizin kullanılması da bir o kadar doğal. Ne de olsa, 3 sabitini arg1 değeri ile temsil etmeden de kendi başına kullanabiliyoruz, fonksiyon değerlerini de ad vermeden kullanabilmeliyiz.

çarp2'nin tanımı yüksek dereceli fonksiyonlara fonksiyon geçirilmesini kolaylaştıran #() ile yapılıyor. Tek kullanımlık fonksiyonların simgesel bir değişkene atanmadan adsız kullanımını kolaylaştıran bu gösterimde, % ilk argümanı temsil ederken, %n n nolu argümana karşılık gelir.

Bazı yüksek dereceli fonksiyonlar ve kullanımlarına dair örnekler aşağıdaki tabloda verilmiştir. Yorumlayıcının oku-değerlendir-bas döngüsünün değerlendirme aşamasında uyguladığı iki temel fonksiyondan biri olan apply—diğeri eval'dir—ilk argümanındaki fonksiyonu daha sonraki argümanlara uygular. Bu işlem sırasında, son argümandaki kap parçalanarak eleman sayısı kadar farklı argüman olarak uygulanması istenen fonksiyona geçirilir. comp fonksiyonu, matematikten bildiğimiz bileşke fonksiyon yaratma işlecidir ve aldığı 0 ya da daha fazla sayıda fonksiyonun bileşkesi olan bir fonksiyon döndürür. juxt parametre listesindeki fonksiyonları ayrı ayrı bilahare geçirilecek argümanlara uygulayacak bir fonksiyon döndürerek işini görür. memoize fonksiyonu argümanındaki fonksiyonun daha hızlı çalışmasını sağlayabilmek adına daha önce yapılan hesaplamaları önbellekte tutup tekrar hesaplamanın önüne geçerek hızlandırmaya çalışan bir fonksiyon döndürür. Son olarak, partial fonksiyonu ilk argümanındaki fonksiyonun istediğinden az sayıda argümanla uygulanmasını olanaklı kılarak geri kalan argümanları bekleyen bir başka fonksiyon döndürür.

Bazı yüksek dereceli fonksiyonlar
FonksiyonİşlevKullanım
applyFonksiyon uygulama(apply fn arg & diğer-arglar)değer
compFonksiyon bileştirme(comp & fnler)fn
juxtFonksiyon uygulama(juxt fn & diğer-fnler)fn
memoizeÖnbellekli fonksiyon uygulama(memoize fn)fn
partialKısmi fonksiyon uygulama(partial fn arg & diğer-arglar)fn

user=> (apply * 2 3 (range 4 10)) → 362880
user=> (def fog (comp #(* % %) #(+ %1 %2)))
#'user/fog
user=> (fog 3 4) → 49
user=> ((juxt * + /) 6 4) → [24 10 3/2]
user=> (def önbellekli-f (memoize f))
#'user/önbellekli-f
user=> (önbellekli-f ...) → ...
Seçtiğimiz örneğin basit olması nedeniyle yapılan tek deyim uzunluğundaki tanımlar sizi yanıltmasın; istenecek olursa, tıpkı kıvrımlı ayraçlarla ({}) belirtilen Java bloklarında olduğu gibi, do özel formunu kullanarak gövdeyi istediğimiz sayıda deyimden oluşacak biçimde uzatabiliriz. İçerdiği formları baştan sona ardışık bir şekilde işleyen bu form, sonucu olarak son işlenen formun döndürdüğü değeri döndürür. Mesela, çarp2'nin tanımının bir selamlama ile genişletilmesi istenecek olursa, bu #(do (println "Selam") (* %1 %2)) tanımı ile yapılabilir. Benzer bir değişikliğin fn formunun kullanıldığı örnekte yapılmasına gerek yoktur; bu form gizli bir do ile çevrelenmiş gibi çalışır.

Temel Veri Türleri


Desteklenen temel veri türlerine geçmeden şu noktayı vurgulayalım: Clojure dinamik türlü bir programlama dilidir. Yani, Clojure'da tanımlayıcılar dil işlemcisine yardım etmek amacıyla türe dair üstbilgi ile nitelenmezler; bir tanımlayıcının türü kendisine yapılan ilkleme/atama sonrasında temsil etmeye başlayacağı değere göre belirlenir. Statik türlemeli dillerde derleyicinin denetimi sonrasında yazılmasına izin verilmeyecek bazı işe yarar programların yazılıp çalıştırılabileceği anlamına gelen bu özellik, kimilerinin düşündüğünün aksine, Clojure'un zayıf bir tür denetimine sahip olması demek değildir. Kaynak kodun işlenmesi esnasında tüm işlemlerin tür denetimini yapan Clojure, türleme hatasına sahip herhangi bir işlemin icra edilmesine izin vermez. Özetlemek gerekirse, Clojure hem dinamik türlüdür hem de kuvvetli türlemeye sahiptir.4

Aşağıdaki tablodan da görülebileceği gibi, Clojure Java programlama dili ile ortak temel türlerden değerleri Java platformundaki sarmalayıcı sınıfların nesneleri olarak temsil eder. Biçimlendirme yapılmadığı müddetçe, tamsayılar Long, gerçel sayılar ise Double türüyle temsil edilirler. İstenecek olursa, Java'daki ilkel türlerle aynı adlara sahip fonksiyonlardan biri kullanılarak biçimlendirme yapılabilir. Örneğin, (byte 3) normalde Long olarak ele alınacak 3'ü Byte türlü bir sabite çevirecektir.

Clojure'daki temel türler
GösterimTürKavram
\ajava.lang.CharacterKarakter
"Katar"java.lang.StringKarakter katarı
5java.lang.LongTamsayı
5N*clojure.lang.BigInt
5.0java.lang.DoubleGerçel sayı
5.0Mjava.math.BigDecimal
22/7, (/ 22 7)clojure.lang.RatioKesirli sayı
(> 1 0)java.lang.BooleanMantıksal değer
'simgeclojure.lang.SymbolTanımlayıcı adı, simgesel sabit, anahtar değeri
:anahtarclojure.lang.KeywordSimgesel sabit, anahtar değeri
*: Clojure 1.3 ve sonrası.

Kayan noktalı sayı aritmetiğinde ortaya çıkabilecek taşmalar Java'da olduğu gibi kullanılmakta olan sayı formatındaki özel değerler ile temsil edilirken🔎, tamsayılarda sınır aşımı programcıya ArithmeticException ayrıksı durumu ile bildirilir. Bunun yerine Long'dan clojure.lang.BigInt'e bir genişletme istenecek olursa, aritmetik işlem adı sonuna ' eklenmesi yeterli olacaktır. Buna göre, (+ 1 Long/MAX_VALUE) ayrıksı duruma neden olurken, (+' 1 Long/MAX_VALUE) 9223372036854775808N değerini döndürecektir.5

İki tamsayının oranını temsil eden kesirler, pay ve paydanın sadeleştirilmesiyle elde edilen iki java.math.BigInteger (clojure.lang.BigInt değil!) nesnesi olarak tutulurlar. Her işlemin bu BigInteger nesnelerden yararlanarak yapılması ve nihai sonucun bunu takip eden sadeleştirmeden sonra elde edilmesi, kesirli sayılarla işlemlerin yavaş olmasına neden olur. Bundan dolayıdır ki, Clojure sadeleşme sonucu paydası 1 olan kesirleri tamsayıya dönüştürerek yoluna devam eder. Sizin de yüksek duyarlık gerektiren hesaplar dışında kesirleri kullanmaktan kaçınmanız yerinde olacaktır.

Lisp ailesi dışındaki programlama dillerinde pek görünmeyen simgeler, tanımlayıcı adlarının türü olarak tanımlanabilir. Bundan ne kastettiğimizi (def i 5) özel formunu inceleyerek açmaya çalışalım. Bu tanımlama sonrasında i simgesi 5 değerine sahip bir değişken olarak ortama eklenir. Bir diğer deyişle, bu tanımla birlikte i 5'i simgelemeye başlar. Bu noktadan sonra, i'ye atıfta bulunulduğunda yorumlayıcı i yerine i simgesinin ortamdaki değeri olan 5'i koyacaktır. Örneğin, (+ i 3) (+ 5 3) haline dönüştürülecektir. Ancak, istenecek olursa simgenin önüne ' konulmak suretiyle bu dönüşümün önüne geçilebilir ve simgelenen değer yerine simgenin kendisi kullanılabilir. Böylesine bir kullanım, hünerini içerdiği tutanak değerlerinin anahtarlar eşitliklerini denetleyerek sergileyen eşlemlerde (ve diğer arama yapılarında) oldukça sık görülür. Çünkü, simgelerde eşitlik denetimi çok hızlı yapılabilir. Aynı özellikten anahtar türü için de bahsedebiliriz. : ile başlayan anahtarlar, başka bir şeyi göstermeyen simgeler olarak düşünülebilir. Yani, yapılacak bir tanım yardımıyla bir değeri gösteren simgelerin aksine, anahtarlar herhangi bir değeri simgelemez. Aynı zamanda, simgelerin program metnindeki her geçişi diğer geçişlerinden farklı iken, herhangi bir anahtarın tüm geçişleri aynıdır.

Veri Kapları


Clojure tarafından sağlanan veri kapları doğrusal kaplar (liste, vektör🔎), kümeler, eşlemler🔎 ve karakter katarları🔎 olmak üzere dört kategoride incelenebilir. Kategoriler veri kaplarını eşitlik temelinde denklik sınıflarına ayırmakta da kullanılır. Eşit addedilmeleri için kapların aynı türden olması gerekmez; dolaşıldıklarında karşılıklı elemanlarının eşit olduğu görülen aynı kategorideki tüm kaplar birbirine eşittir.

Clojure'da desteklenen veri kapları, aşağıdaki tablodan da görülebileceği gibi, yapıcı görevi gören fonksiyonların yanısıra özel sözdizimi yardımıyla da yaratılabilir. Dikkat edilirse, küme ve eşlemler için, Java Veri Kapları Çerçevesi'ndekileri anımsatan, farklı yapıcılar sağlandığı görülecektir. Bu, aynı zamanda söz konusu kap için (class kap) deyimiyle keşfedilebilecek farklı bir gerçekleştirim seçildiği anlamına gelmektedir.

Clojure destekli kaplar
CinsYapıcıÖzel gösterim
Liste(list 1 2)'(1 2)
Vektör(vector 1 2)[1 2]
Eşlem{:Adana 1 :İzmir 35}
(hash-map :Adana 1 :İzmir 35)
(sorted-map :Adana 1 :İzmir 35)
Küme(hash-set :insan :şempanze)#{:insan :şempanze}
(sorted-set :insan :şempanze)
Kar. katarı(str \a \b \c)"abc"

Clojure'dakilere ek olarak, Java Veri Kapları Çerçevesi'nce sağlanan eşdeğer kaplardan da yararlanılabilir. Aynı kategoride addedilen bir Clojure kabı ile Java eşdeğerinin hemen hemen aynı olduğu söylenebilir. Eşit içeriğe sahip eşdeğer kaplar birbirine eşittir; Clojure kaplarına uygulanan fonksiyonlar Java eşdeğerlerine de uygulanabildiği gibi, Clojure kapları Java nesnesi olarak da kullanılabilir. İlişkin örnekler aşağıda verilmiştir.
user=> (import '(java.util ArrayList Vector))
java.util.Vector
user=> (def java-ls (ArrayList.))
#'user/java-ls
user=> (.add java-ls 1) → true
user=> (doto java-ls (.add 2) (.add 3)) → #<ArrayList [1, 2, 3]>
user=> (def java-vek (Vector. java-ls))
#'user/java-vek
user=> (= java-vek '(1 2 3) [1 2 3] java-ls) → true
user=> (= #{1 2} '(1 2)) → false
user=> (map #(* % %) java-vek) → (1 4 9)
user=> (reduce (fn [a b] (if (> a b) a b)) '(80 70 85)) → 85
user=> (filter odd? [1 2 3 4 5]) → (1 3 5)
user=> (remove odd? #{1 2 3 4 5})) → (2 4)
user=> (.size [1 2 3 4]) → 4
user=> (apply str (map #(Character/toUpperCase %) "Abc")) → "ABC"
Kodu incelerken şu noktaların bilinmesi yardımcı olacaktır: i) import bir veya daha fazla sayıda sınıf dosyasını içselleştirip çalışma ortamında görünür hale getirir, ii) . ile sonlanan sınıf adları Java platformundaki bir sınıfın yapıcılarından birini çağırır, iii) önüne . eklenmiş fonksiyon adları ilk argümanda belirtilen alıcıya ileti göndermekte kullanılır, ve iv) doto makrosu ikinci ve sonrasındaki argümanlarda belirtilen iletileri ilk argümanda belirtilen alıcıya yönlendirir.

Kaplar üzerinde çalışan bazı yüksek dereceli fonksiyonlar
FonksiyonİşlevKullanım
mapKap dönüşümü(map fn k & diğer-kaplar)kap
reduceİndirgeme, özetleme(reduce f k), (reduce f ilk-değer k)değer
filterSüzme(filter fn k)kap
removeSilerek süzme(remove fn k)kap

Clojure ve Java veri kapları arasındaki şu temel farklılık unutulmamalıdır: Java'da kap içeriği uygun iletiler yardımıyla değiştirilebilirken, Clojure'daki tüm kaplar değişmez içeriklidir. Bir başka deyişle, Clojure'da kaplar yaratılmaları sonrasında değiştirilemezler. Değişme etkisinin yaratılması için içerik güncelleme işlemleri yapan fonksiyonların dönüş değerlerinin kullanılması gerekir.
user=> (def çete '("Veli" "Selami"))
#'user/çete
user=> (def yeni-çete (conj çete "Ali"))
#'user/yeni-çete
user=> çete → ("Veli" "Selami")
Düşük maliyetli bir biçimde gerçekleştirilebilen bu stratejinin (bkz. 1, 2) temelinde programcıyı değişken içerikli kaplardan uzak tutmak suretiyle çok izlekli program yazma bağlamında daha kolay sınanabilir kod üretmeye sevketmektir. String sınıfının🔎 sabit içerikli olmasının temelinde de yatan bu yaklaşım, değişmeyecek bir kabın birden çok izlekten sakıncasız bir biçimde kullanılabileceği gerçeğinden yararlanır. Aslında, Java Veri Kapları Çerçevesi de dikkatle bakan gözlere aynı mesajı vermektedir: arayüzlerde yer alan tüm içerik güncelleyici iletilerin başlığı UnsupportedOperationException ayrıksı durumunu içerir. Clojure kapları buna dayanarak, Clojure'a özel fonksiyonlara ek olarak [Java kodu ile birlikte çalışmak adına] Veri Kapları Çerçevesi'nin ilişkin arayüzünü gerçekleştirdiklerinde söz konusu iletileri desteklemediklerini bildiren UnsupportedOperationException ayrıksı durumunu fırlatacak şekilde gerçekleştirir.
user=> (def vek [1 2])
#'user/vek
user=> (.size vek) → 2
user=> (.toString vek) → "1 2"
user=> (.add vek 3) → java.lang.UnsupportedOperationException (NO_SOURCE_FILE:0)
map, reduce gibi pek çok fonksiyonun ayırt etmeksizin tüm kaplara uygulanabilmesi, bu tür fonksiyonların diğer Lisp ailesi dillerinde olduğu gibi liste veya bir diğer türden kap almak yerine clojure.lang.ISeq arayüzünü gerçekleştiren bir kap alıyor olmasıyla mümkün olur. Buna göre, Clojure listelerinin gerçekleştirimini sağlayan Java sınıfının aşağıdaki gibi olduğunu söyleyebiliriz.
package clojure.lang;

public class PersistentList extends Obj implements ISeq, ... { ... }
ISeq arayüzü, eleman sayısını döndürüren count ve ilk argümanındaki kaba ikinci argümanındaki değerin eklenmiş halini döndüren conj'un yanısıra, yegâne argümanındaki kabın dolaşılmasına yarayan bir liste döndüren seq iletisini içerir.

Son olarak değineceğimiz nokta, vektör veya eşlem gösteren bir simgenin fonksiyon adı konumunda kullanılabilecek olmasıdır. Aşağıdaki kod parçasında olduğu gibi, bir vektöre indis, bir eşleme ise anahtar değeri verilecek olursa ilişkin kap içindeki o indis veya anahtar karşılık gelen değer döndürülür.
user=> ([1 2 3 4] 1) → 2
user=> (plaka-nolar :İzmir) → 35

Fonksiyon Tanımı, Yeniden


Tanımının uzamasıyla birlikte fonksiyon gövdesinin anlaşılması da zorlaşacaktır. Bu etkiyi azaltmak amacıyla, gerçekleştirimimize yorum ve üstbilgi eklemeyi düşünebiliriz. Böyle bir durumda, makro işlemcisi tarafından def ve fn formlarına dönüştürülecek olan defn makrosunu kullanmamız daha yerinde olacaktır.

Selamlar.clj
(import '(java.util Calendar GregorianCalendar))

(defn selamlar
  "Argümanında geçirilen kişilere selam verir. Bunu yaparken aşırı yüklemeden yararlanır."
  {:yazar "Tevfik AKTUĞLU" :tarih (GregorianCalendar. 2012 Calendar/JANUARY 7)}
  ([tanıdık] (println "Selam," tanıdık))
  ([x y & diğerleri] (println "Selam millet!"))
  ([] (println "Selam, kim olursan ol!")))
...
Yukarıdaki fonksiyon tanımı, üç farklı parametre listesine göre üç gövde sağlamaktadır. Bir bakıma, hepsinin adı selamlar olan üç ayrı fonksiyon tanımlanmaktadır. Bu fonksiyonlardan ilki tek argümanlı iken, ikincisi 2+ argüman beklemektedir; son gövde tanımı ise argümansız kullanım durumunda işlenecektir. Değişken sayıda argüman alan seçenekte, ikinci argüman sonrasında geçirilecek tüm değerlerin bir listeye konulup & imini takip eden parametreye (diğerleri) karşılık gelen argümanda geçirileceği söylenmektedir.

defn makrosu fonksiyon tanımında kullanılan diğer formların sunmadığı iki olanak sunmaktadır. Bunlardan ilki, tanımlanmakta olan fonksiyonun adını takiben sağlanan ve daha sonra doc fonksiyonuyla sorgulanabilecek dokümantasyon yorumudur. İkinci olanak ise, programcının gereksinimlerine göre seçeceği üstbilgilerin fonksiyon tanımına iliştirilmesidir. Örneğimizde, dokümantasyon yorumları sonrasındaki üstbilgi eşlemi vasıtasıyla kodu yazan kişi ve en son güncelleme tarihi bilgilerini selamlar simgesine iliştiriyoruz.
;;Sınıf yolu üzerinde bulunan Selamlar.clj dosyasındaki formları yükler. 
user=> (load "Selamlar") → nil
user=> (selamlar) → nil
Selam, kim olursan ol!
user=> (selamlar "Tevfik") → nil
Selam, Tevfik
user=> (selamlar "Ali" "Veli" "Selami") → nil
Selam millet!
user=> (doc selamlar) → nil
------------------------
user/selamlar
([tanıdık] [x y & diğerleri] [])
  Argümanında geçirilen kişilere selam verir. Bunu yaparken aşırı yüklemeden yararlanır.
user=> (meta (var selamlar))
{:ns #<Namespace user>, :name selamlar, :file "Selamlar.clj",..., :tarih ...}
selamlar'a alternatif olarak sunulan ve 0+ argüman geçirilerek çağrılabilecek aşağıdaki selamlar2 fonksiyonu, yeni bazı şeyler sunmakta. Bunlardan ilki, parametre listesi sonrasındaki önkoşul tanımıdır. Bilenlerin Eiffel programlama dilinden anımsayacağı sözleşme temelli tasarımın (İng., Design by Contract) bir parçası olan önkoşul (İng., precondition), fonksiyonun her çağrılışı öncesinde denetlenir. Sonucun olumsuz olması, sağlıklı çalışma için karşılanması zorunlu görülen bir koşulun oluşmadığı anlamına gelir ve bu bilgi fonksiyon çağrılmaksızın programın bitirilmesini takiben uygun bir mesajla kullanıcıya bildirilir. Mesela, selamlar2 örneğinde selam verilecek kişileri temsil eden argümanların toplandığı listenin 3 veya daha az sayıda elemanı olması bir önkoşul olarak tanımlanıyor. Tamamlayıcı bir denetim, fonksiyon çağrısının sona erdiği noktada karşılanması zorunlu görülen durumların belirtilmesini mümkün kılan sonkoşulları listeleyen :post ile yapılabilir.
...
(defn selamlar2
  "cond formundan yararlanarak argümanında geçirilen kişilere selam verir. Bu arada, önkoşul denetimiyle 4 ve daha fazla sayıda kişiden oluşan gruplara selam vermeyerek asayişi korur."
  {:yazar "Tevfik AKTUĞLU" :tarih (GregorianCalendar. 2012 Calendar/JANUARY 7)}
  [& ahali] {:pre [(< (count ahali) 4)]}
  (let [nüfus (count ahali)]
    (cond
      (> nüfus 1) (println "Selam millet!")
      (= nüfus 1) (println "Selam," (first ahali))
      :aksi-takdirde (println "Selam, kim olursan ol!"))))
Fonksiyon tanımında dikkat çekilmesi gereken bir diğer nokta, blok değişkenlerin tanımını olanaklı kılan let özel formudur. Kendisine sağlanan vektörün tek sayılı sıralardaki elemanlarını değişken adı olarak kabul eden let, değişkeni takip eden değeri ilkleme amacıyla kullanır. Buna göre örneğimizdeki let formu, nüfus adına sahip ve argümanda geçirilen listenin eleman sayısıyla ilklenmiş bir yerel değişken tanımlamaktadır. Tanımlanan blok içindeki cond makrosu ise, C-temelli dillerdeki switch-case yapısına benzer bir görev görür: koşulları tepeden aşağıya sıralı bir biçimde sınar ve doğru olduğunu gördüğü ilk kolun formunu işler. Tüm durumların kapsanması için son kola her zaman doğru olacağı bilinen bir koşul konmalıdır. false ve nil dışındaki tüm değerler doğru kabul edildiği için, örneğimizde bu görevi :aksi-takdirde anahtarı görmektedir.
user=> (selamlar "Ali" "Veli" "Selami" "Nuri") → nil
java.lang.AssertionError: Assert failed (< (count millet) 4) (NO_SOURCE_FILE:0)
user=> (find-doc "selamlar") → nil
------------------------
user/selamlar
([tanıdık] [x y & diğerleri] [])
  Argümanında geçirilen kişilere selam verir. Bunu yaparken aşırı yüklemeden yararlanır.
-------------------------
user/selamlar2
([& ahali])
  cond formundan yararlanarak argümanında geçirilen kişilere selam verir. Bu arada, önkoşul denetimiyle 4 ve daha fazla sayıda kişiden oluşan gruplara selam vermeyerek asayişi korur.

Özyineleme


Yazımızın son kısmında fonksiyonel programlama dillerinin birincil yineleme yöntemi olan özyinelemeye değineceğiz. Bu yapmamızın nedeni, Clojure'un komutsal dillerden gelenlere tanıdık gelecek türden döngüleri desteklememesi değil. Elbette ki, dilin kendisinde olmasa bile Clojure'daki gibi bir makro olanağınız varsa, bildik tüm döngüleri ve fazlasını özyineleme kavramını kullanarak tanımlayabilrsiniz. Derdimiz, fonksiyonel programlamadan optimize edilen özyinelemeli çağrılara alışmış olanlara ufak bir uyarıda bulunmak. Çok sallanmadan faktöryel fonksiyonunu gerçekleştiren aşağıdaki tanımla başlayalım.
(defn ! [n]
  {:pre (pos? (inc n))}
  (if (<= n 1)
    1
    (* n (! (dec n)))))
Argümanının eksi olmaması önkoşuluna sahip gerçekleştirimimiz, özyinelemenin bitiş koşulu olarak argümanının 1 veya 0 olmasını seçmiş ve bu durumda 1 döndürüyor. Aksi takdirde, tümevarım adımında ifade edildiği gibi, problem kendi türünden daha basit bir probleme indirgenerek çözüm sağlanıyor: argüman ile argümanın bir eksiğinin faktöryeli çarpılıp sonuç olarak döndürülüyor. Ancak, bildirimsel (İng., declarative) olması nedeniyle doğruluğu kolayca kanıtlanabilecek gerçekleştirimimizin—ne de olsa, çözümü programlama dünyasına özel yapıları kullanarak sağlamaktansa problem tanımını birebir çevirerek sağlıyoruz—sonsuz döngülerle boğuşmuş olanlara pek de yabancı gelmeyecek bir sorunu var: bitiş koşuluna uzak bir ilk durumun verilmesi durumunda, özyinelemeli çağrıların sayısı artacak ve argümanların yerleştirildiği çağrı yığıtı dolarak hesaplamamız sonuç döndürmeden StackOverflowError ile sona erecektir.
user=> (! 5) → 120
user=> (! 100000) → java.lang.StackOverflowError (NO_SOURCE_FILE:0)
Ortaya çıkan sorun iki şekilde çözülebilir: i) ürettiği ara sonuçları biriktirici argümanlar yoluyla bir sonraki çağrıya aktaran özyinelemeli bir çözümle, ii) özyineleme yerine döngü kullanarak. Gelin birinci şıkkın gerçekleştirimi olan aşağıdaki koda bir göz atalım.
(defn !2 [n]
  {:pre (pos? (inc n))}
  (letfn [
    (!-iç [n biriktirici]
      (if (<= n 1)
        biriktirici
        (!-iç (dec n) (* biriktirici n))))]
    (!-iç n 1)))
Örneğimizde, kullanıcının !2'ye sağladığı değer yerel fonksiyona o ana kadarki hesaplamanın özeti olarak niteleyebileceğimiz biriktiricinin ilk değeri ile birlikte geçiriliyor. Kullanıcının görmediği !-iç'i incelediğimizde, tümevarım adımının özyinelemeli çağrı sırasında ikinci argümanı o anki ilk argümanla çarparak güncellediğini ve bitiş koşulunun oluştuğu noktada ise 1 yerine biriktirilmiş değeri döndürdüğünü görüyoruz. Bu dönüşümü yapmaktaki amacımız, özyinelemeli çağrıyı fonksiyon içinde en son yapılan iş haline getirmek ve böylece fonksiyonel programlama dillerinde sıklıkla uygulandığını bildiğimiz kuyruk çağrısı eniyilemesinden yararlanmaktır. Gelin görün ki, kazın ayağı öyle değil! JSM üzerine inşa edilmiş olan Clojure, JSM'nin kuyruk çağrısı eniyileme desteği vermemesi nedeniyle bizi utandırıyor.
user=> (!2 100000) → java.lang.StackOverflowError (NO_SOURCE_FILE:0)
Derdimizin çaresi, Clojure'a işi JSM'ye bırakmadan kendi yapabildiği kadarıyla yapmasını söylemek. Bu ise, aşağıdaki gibi özyinelemeli çağrıların olduğu yerlerde, fonksiyon adı yerine recur yazmakla mümkündür. recur özel formu, kuyruk konumunda yapılan özyinelemeli çağrıları arka planda goto benzeri birer sıçrama komutuna çevirir. [Sadece özyinelemeli çağrılar eniyilenir; genel kuyruk çağrısı eniyilemesi yapılmaz!] Bu, çağrı yığıtında ilk çağrıdaki argümanların işgal ettiği yerlerin yeniden kullanılacağı ve dolayısıyla sabit bellekle işimizin halledileceği anlamına gelir; ayrıca, fonksiyon çağrıları yerini daha ucuz olan sıçrama komutlarına bırakacağı için çalışma hızı da olumlu etkilenecektir. Bunu time makrosu yardımıyla görebilirsiniz.
(defn !3 [n]
  {:pre (pos? (inc n))}
    (letfn [
      (!-iç [n biriktirici]
        (if (<= n 1)
          biriktirici
          (recur (dec n) (* biriktirici n))))]
      (!-iç n 1)))
user=> (time (!3 2000)) → "Elapsed time: 20.266474 msecs" 33162...
user=> (time (!2 2000)) → "Elapsed time: 24.969519 msecs" 33162...
Bir diğer çözüm yöntemi loop özel formundan yararlanır. Döngü tutkunu olanları özyinelemeye alıştırmak için önerilebilecek bu form, eniyilenmiş özyinelemeli kuyruk çağrısı çözümüne yakın, hatta daha iyi bir çalışma hızı sağlar. Hızı daha da iyileştirmek isteyenlere önbellekli fonksiyon uygulaması olanağını sağlayan memoize fonksiyonu önerilir. Ancak; önceden yapılmış bazı hesaplamaları tekrar hesaplamaktansa kaydettiği sonucu kullanarak işini gören bu yüksek dereceli fonksiyonun argümanları dışında çağrı ortamına bağımlılığı bulunan fonksiyonlarla çalışamayacağı akılda tutulmalıdır.
(defn !4 [n]
  {:pre (pos? (inc n))}
  (loop [i n, biriktirici 1]
    (if (<= i 1)
      biriktirici
      (recur (dec i) (* biriktirici i)))))
user=> (def !4-hızlı (memoize !4))
#'user/!4-hızlı
user=> (time (!4-hızlı 1900)) → "Elapsed time: 27.253187 msecs" 33162...
user=> (time (!4-hızlı 2000)) → "Elapsed time: 21.0582 msecs" 33162...
Tüm diğer zaman ölçümlerinde olduğu gibi, hıza dönük yorumların beklenen ortalama çalışma hızı düşünülerek yapıldığı unutulmamalıdır; döndürülen değerlerin sisteme ve yüke göre değişmesi, önbellekli uygulama örneklerinde önbellek yönetiminden kaynaklanan beklenmedik farkların oluşması sizi şaşırtmamalıdır.


  1. Java SE 8 ile birlikte, kod örtülerinin bu eksikliği büyük ölçüde gidereceğini söyleyebiliriz. Dolayısıyla, 2013'ün ikinci yarısında yargımızın Java bölümü geçerliğini yitirmiş olacak.
  2. Clojure programlama dilinin resmi sitesine gittiğinizde, Screencast etiketli bir bağlantı gözünüze çarpabilir. Gezegenimizin mutlu insanlarını Clojure diliyle ilgili bir video serisine götüren bu bağlantı, Türkiye'de yaşayanlar için—henüz farkına varılmayan üniversite ve benzeri ortamlardaki mutlu azınlık dışında—alıştık İnternet sansürlerinden birini simgeliyor. [Son kontrol tarihi: 29-Aralık-2011] İşin hazin tarafı, bu durum Wikipedia'nın blip.tv sayfasında, ülkemizin Çin'le birlikte—listede üçüncü bir ülke yok!—anılması yoluyla reklam ediliyor.
  3. İllaki bol pencereli bir geliştirme ortamı istiyorsanız, size bu sayfaya bakmanızı öneririm. Ayrıca, çalışma ortamınızın özelliklerine bağlı olarak, örneklerdeki Türkçe'ye özel harflerde sıkıntı yaşayabilirsiniz. Bunun olası nedeni, kullanmakta olduğunuz Java arşivinin Türkçe'deki bu karakterleri dışlayacak bir kodlama ile derlenip oluşturulmasıdır. Tavsiyem, clojure komutuna öncelik vermeniz; bu da çözüm sağlamıyorsa, Clojure ortamını UTF-8 kodlaması ile derlemeniz.
  4. Aslına bakılırsa, Clojure tanımlayıcılar hakkında tür bilgilerinin sağlanması için isteğe bağlı olarak kullanılabilecek bir düzenek sağlıyor. Ancak, yazımızda bu olanağa değinmeyeceğiz.
  5. Clojure 1.3 öncesinde durum farklıydı. Öncelikle, nitelenmemiş bir sabitin türü Long değil, Integer idi. Ayrıca, taşma ayrıksı duruma sebep olmaktansa söz konusu değeri Integer ise Long'a, Long ise java.math.BigInteger—Clojure 1.3'te olduğu gibi clojure.lang.BigInt değil!—türüne terfi ettirerek sessizce geçiştiriliyordu.

Hiç yorum yok:

Yorum Gönder

Not: Yalnızca bu blogun üyesi yorum gönderebilir.