Bundan üç sene önce enteresan bir konuşma’ya denk geldim. Russ Olsen, insanlığın aya ilk çıkışının öyküsünü anlatıyordu. Beklediğimin aksine, oldukça duygusal ve heyecan vericiydi. Anlatıcının tarzını beğendim ve farklı konuşmalarını izlemeye karar verdim. Daha sonra, fonksiyonel programlamadan bahsettiği bu konuşmasını izledim. Beni tanıyanlar bilir, bir şeylerle alakalı hızlı heyecanlanırım. Heyecanlandım. Fonksiyonel bir dil bulup bir an evvel öğrenmem lazımdı. Aslında üniversitede biraz Haskell öğrenmiştik ama o noktada hiçbir şey hatırlamıyordum. Dolayısıyla gayet yeni bir dil de olabilirdi. O dönem dostum Berk Özkütük’ten aldığım yönlendirmeyle Clojure öğrenmeye başladım.
Bugün ise sizlere bu bol parantezli, tuhaf dil Clojure ile hepimizin bildiği güçlü dil Java’nın ufak bir kıyasını yapmaya çalışacağım.
Alex Miller’in harika kitabı Programming Clojure‘da çok hoşuma giden bir karşılaştırma biçimi vardı. Yazar, çok kullanılan bir projeden open source bir Java kodunu alıp Clojure’da yeniden yazma sürecini anlatıyor ve iki kodu karşılaştırıyordu. Bence hem öğreticiydi, hem de çeşitli faktörleri güzel ortaya koyan bir karşılaştırma biçimiydi. Bu yazıdaki amacım için ben de benzerini yapmaya karar verdim. Altta open source bir Java projesinden bir method var. Anlamak için domain bilgisi gerekmemesi adına, yazarın da yaptığı gibi, StringUtils
class’ını tercih ettim.
Method’a şöyle bir bakalım.
public static int indexOfDifference(final CharSequence cs1, final CharSequence cs2) {
if (cs1 == cs2) {
return INDEX_NOT_FOUND;
}
if (cs1 == null || cs2 == null) {
return 0;
}
int i;
for (i = 0; i < cs1.length() && i < cs2.length(); ++i) {
if (cs1.charAt(i) != cs2.charAt(i)) {
break;
}
}
ıf (i < cs2.length() || i < cs1.length()) {
return i;
}
return INDEX_NOT_FOUND;
}
Method, dokümantasyonunda da yazdığı gibi iki tane CharSequence alıyor ve farklılaşmaya başladıkları index’i dönüyor. Dokümantasyondan bazı örnekler de burada:
* <pre>
* StringUtils.indexOfDifference(null, null) = -1
* StringUtils.indexOfDifference("", "") = -1
* StringUtils.indexOfDifference("", "abc") = 0
* StringUtils.indexOfDifference("abc", "") = 0
* StringUtils.indexOfDifference("abc", "abc") = -1
* StringUtils.indexOfDifference("ab", "abxyz") = 2
* StringUtils.indexOfDifference("abcde", "abxyz") = 2
* StringUtils.indexOfDifference("abcde", "xyz") = 0
* </pre>
Güzel. Şimdi aynısını adım adım Clojure ile yazmaya çalışalım.
Önce Clojure syntax’ının üzerinden hızlı bir şekilde geçelim. Clojure syntax’ı çok basit, çünkü sadece listeler var. Örneğin:
(* (+ 1 3) 5)
Parantez açılışı bir listenin başladığını belirtiyor. Listelerin özelliği ise şu, ilk elemanları bir fonksiyon, kalan elemanları da o fonksiyonun argümanları.
Üstteki örnekte, *
(çarpma fonksiyonu)’na (+ 1 3)
ve 5
argümanlarını veriyoruz.
(+ 1 3)
, yine bir liste olduğu için, ilk elemanı fonksiyon ve kalanları argümanları, dolayısıyla 4’e evaluate ediyor. 5 ise kendisine evaluate ediyor. Dolayısıyla Clojure’un bu expression’ı değerlendirişi şöyle:
(* (+ 1 3) 5)
(* 4 5)
20
Daha sonra şu bilgiyi verelim,
Clojure String’leri seqable
. Bu terimi şimdilik önemsemenize gerek yok, bilmemiz gereken kendilerine char collection
gibi davranabildiğimiz.
Şimdi fonksiyonumuzu yazmaya başlayabiliriz.
İlk olarak, elemanlarının index’ini bir collection içinde dönen bir fonksiyon oluşturalım. Yine alttaki listemizin üzerinden geçersek, defn
yeni fonksiyon oluşturmak için kullandığımız sembol, sonraki eleman fonksiyonun ismi, daha sonra köşeli parantezlerin içindeki kısım fonksiyonun argümanları, ve sonra gelen liste de fonksiyonun ne yapacağını deklare ettiğimiz yer. Bir Lisp ile ilk karşılaşınız ise syntax’ı biraz tuhaf bulmanız normal, lütfen korkmayın ve okumaya devam edin. :))
(defn get-indexed [coll]
(map-indexed vector coll))
(get-indexed "trabzon")
;; => ([0 \t] [1 \r] [2 \a] [3 \b] [4 \z] [5 \o] [6 \n])
İki collection’ı karşılaştırmak için map
fonksiyonunu kullanabiliriz:
(map = "tamam" "takim")
;; => (true true false false true)
map
fonksiyonu, input olarak bir fonksiyon ve collectionlar alıp, fonksiyonu collectionlara dağıtıyor. Burada, =
fonksiyonunu collectionların elemanlarına tek tek dağıtıyor.
Yani map
‘in yaptığı checkler, sırasıyla, t == t
, a == a
, m == k
, a == i
ve m == m
.
Biz, string’lerin ayrışmaya başladığı noktayı arıyoruz, yani map
‘in ilk false
döndüğü yeri. Az önce yazdığımız, index’leri bulmamızı sağlayan fonksiyona map
‘in sonucunu verelim:
(get-indexed (map = "tamam" "takim"))
;; => ([0 true] [1 true] [2 false] [3 false] [4 true])
Şu an, yapmamız gerekenden fazlasını bile yaptık. bulmamız gereken ilk false
‘ın index’i. Yani true
‘lardan kurtulursak, ilk elemanın ilk elemanı sonucumuz olacak. filter
tam da bu işi yapıyor:
(filter
(fn [inp] (-> inp second false?))
(get-indexed (map = "tamam" "takim")))
;; => ([2 false] [3 false])
Filter fonksiyonumuza biraz daha yakından bakalım:
(fn [inp] (-> inp second false?))
Bir input alıyor, baştaki ->
inputa sırasıyla sonraki fonksiyonların uygulanmasını sağlıyor. Yani ilk önce inputun ikinci elemanını alıyor, daha sonra bu elemanı sıradakı fonksiyon false?
‘a veriyor ve sonucu dönüyor. false?
‘daki soru işaretinin sebebi, Clojure’da predicate’ların (true
ya da false
dönen fonksiyonlar) ? ile bitmesi (bu bir kural değil, Clojure developer’larının takip ettiği bir alışkanlık, siz isterseniz ? ile bitmeyen predicateler da yazabilirsiniz, ama sizi sevmeyiz ve selamınızı almayız.).
İlk elemanın ilk index’ini de alırsak(ffirst
bu işi yapan bir Clojure fonksiyonu):
(ffirst
(filter
(fn [inp] (-> inp second false?))
(get-indexed (map = "tamam" "takim"))))
;; => 2
Sonucumuzu bulduk. Şimdi tam çözümümüze şöyle bir bakalım:
(defn get-indexed [coll]
(map-indexed vector coll))
(defn compare-colls [pred coll1 coll2]
(filter
(fn [inp] (-> inp second false?))
(get-indexed (map pred coll1 coll2))))
(defn index-of-diff [coll1 coll2]
(ffirst (compare-colls = coll1 coll2)))
if
ler, çirkin -1’ler, looplar yok. Ne yaptığı bariz olan basit fonksiyonlar var.
Java kodunda 2 tane if
, 2 tane olası return
noktası ve 1 tane for vardı ve 15 satırdı (Short circuit’taki if
‘i talep üzerine saymamaya karar verdim, doğru, onu saymak biraz haksızlık oldu).
Clojure çözümümüzde herhangi bir if veya for yok. Tek return noktası fonksiyonun sonu, ve 8 satır. Dolayısıyla Clojure kodu daha basit ve daha okunabilir. Aynı zamanda daha rahat extend edilebilir, örneğin gördüğünüz gibi compare-colls
fonksiyonu Java match’inin aksine herhangi bir predicate alabiliyor, yani diyelim ki siz ilk difference’in değil de ilk aynı noktanın index’ini istiyorsunuz, hay hay:
(defn index-of-same [coll1 coll2]
(ffirst (compare-colls not= coll1 coll2)))
(index-of-same "kitap" "hitap")
;; => 1
Sadece yazdığımız diğer iki fonksiyonu yeniden kullanarak, logic’in daha farklı olduğu başka bir problemin de çözümünü tek satırda bulabildik. Java örneğinde aynısını yapmaya çalışsak, belki birkaç yeni Comparator Class
açıp fonksiyonumuza onları pass’leyebilirdik, ancak ya Comparator
değil de fonksiyon passlememiz gerekseydi? O zaman herhalde tüm kodu kopyalamak durumunda kalacaktık, çünkü Java’da Clojure’da olduğu gibi fonksiyonlar first class citizen değil. (Lütfen yazının sonundaki Not 3’e bakın.)
Peki -1
‘ler, if
‘ler neden yok? Bu, imperative ve declarative tarzların farkı. Imperative tarzda, yapısı gereği, her case’de ne yapılması gerektiğini koda adım adım belirtmeniz gerekiyor. Fonksiyonel yaklaşımlarda ise koda tek tek her case’i anlatmaktansa beklediğiniz sonucu deklare ediyorsunuz (Bana ikincisi false
olan liste elemanlarını ver diyorum; bir liste oluştur, ikinci eleman false
mu diye bak, her false
bulduğunda listeye ekle, sonra listeyi dön, demiyorum.). Fonksiyonel yaklaşımımız bu örnekte bizi counter gibi local variable’lardan da kurtarıyor.
Daha basit, hataya daha az müsait, döngüler, değişkenler olmayan ve daha kolay extend edilebilen kod. Ayrıca yazması çok daha eğlenceli (REPL Driven Development’tan da bilahare başka bir yazıda bahsedeceğim.).
Şimdi, Paul Graham’ın efsane makalesi Beating The Averages’a gidelim.
Bu iki dili kullanan iki startup düşünelim. Eşit sayıda mühendisleri olsun ve bu mühendisler eşit seviyede çalışsın. Sizce market için aralarındaki yarışı hangisi kazanır? Hangisi average kalır, hangisi öne geçer? :)
Tamam da hocam, Java’daki o çirkin ifler, -1’ler edge case’ler içindi. Senin çözüm onları hallediyor mu?
Evet, hallediyor. (Java çözümü ikinci durum için -1 dönüyor, biz de istersek basitçe dönebiliriz (or -1
ile wrap’leyerek) ancak nil
dönmek Clojure için daha idiomatic)
(index-of-diff "tamam" "takım")
;; => 2
(index-of-diff "" "of")
;; => nil
(index-of-diff "hmm" "yoo")
;; => 0
Tamam, fena durmuyor, ama ya Clojure’u seçen startup’taki yazılımcılar bir anda emekli olmaya karar verirse? O zaman ne yapacak o startup? Java developer’ı bulmak daha kolay değil mi?
Bu harika soru için teşekkür ederim. Arkadaşlar Clojure’un en güzel özelliği bu. Clojure developer’a ihtiyacınız yok, Clojure developer’ları siz yetiştirebilirsiniz. Syntax saçma sapan biçimde basit. Dilin içindeki her şey(hemen hemen her şey) listelerden oluşuyor. Listelerin ilk elemanı fonksiyon, kalanları da o fonksiyonun argümanları. Bu kadar. Yeni mezunlar en iyi Clojure developer adaylarınız. Çok kısa vadede üretken hale geleceklerine eminim.
Dediğin doğru olsa bunun başarılı bir örneği olurdu.
Var, Güney Amerika’nın en önemli dijital bankası.
Tamam ama bu dille her şeyi yapamazsın. Değil mi?
Java ile yapabildiğin her şeyi yapabilirsin. :) Clojure general purpose bir dil. Örneğin şu anki iş yerimde web servisleri geliştirmek için kullanıyoruz. Hatta bir post’ta da beraber bomba gibi bir API yaparız. Şöyle kalabalık bir meydanda. Güzel olabilir. Mümkün de görünüyor.
Peki bu dilin iş imkanları nasıl?
Türkiye’de maalesef bildiğim bir imkanı yok. Avrupa’da, Güney Afrika’da, Amerika’da opsiyonlar var. Kendi adıma ben Clojure öğrenmeye başladıktan sonra ilk işimi iki ay içinde, ikinciyi 3 hafta içinde buldum.
Tamam, dil harikaymış. Ben de öğrenmek istiyorum. Burdan nereye gitmeliyim?
Fonksiyonel bir dilde henüz fluent değilseniz Russ Olsen’in harika kitabı Getting Clojure‘ı öneririm. Bundan sonra şundan devam edebilirsiniz. Eğer fonksiyonel bir dilde zaten akıcıysanız direkt ikinciden başlayabilirsiniz.
Umarım ilginizi çekmeyi başarabilmişimdir. Sorunuz olursa ulaşmaktan çekinmeyin.
Not 1: Üstteki soruları kafamdan uydurdum. Kendi kendime yazıp cevapladım, aralarda da kendime teşekkür ettim. Delilik var.
Not 2: map
‘e birden fazla collection verildiği zaman map bir collection’daki elemanlar biter bitmez operasyonunu bitiriyor. Dolayısıyla "ta"
ve "tamam"
örnekleri için, Clojure kodumuz Java kodu gibi 2 değil, nil
döner. Bunu düzeltmek için Clojure koduna ufak bir if
check eklememiz lazım. Ancak problemin asıl amacından bağımsız olduğu için bu kıyasta o case’i atladım.
Not 3: Uzun zamandır Java yazmıyorum. Java’da neyin mümkün olup, neyin olmadığı hakkındaki iddialarım ve eleştirilerimde yanlış ya da eksik olduğum yerler varsa ve bunu okuyan bir Java developer bana bildirme inceliğinde bulunursa zevkle düzeltirim.
Not 4: defn
ile fonksiyon oluşturuyorsak fn
ile ne yapıyoruz diye düşünmüş dikkatli bir okuyucu varsa, fn
ile anonymous, yani isimsiz fonksiyon oluşturuyoruz. Zaten defn
de (def name (fn ...))
‘e extend eden bir macro.
Not 5: Macro mu ne? Arkadaşlar dilin her özelliğini iki satırlık post’ta anlatamam :p Bilahare de ondan bahsederiz.
Feedback 1: Java’daki bir return noktasının short circuit için olduğuna yönelik bir feedback aldım. Doğru, üstteki if ve olası return noktası rakamlarını bir azaltıyorum. :)