Bilgisayar bilimlerinin temel yapı taşlarından biri olan veri yapılarından bahsetmek istiyorum. Tanım olarak veri yapısı verilerin bilgisayarlar tarafından etkili bir şekilde kullanılanılabilmeleri için belirli yollarda saklanması anlamına gelir. Önceki yazımda veri yapılarına temel teşkil eden bazı basit veri türlerinden bahsetmiştim. Bu yazımda ise Clojure dilinde önceden tanımlanmış olan veri yapılarını tanıyor olacağız.

Bu yazıyı okumadan önce serinin diğer yazılarından faydalanmak isteyebilirsiniz:

  1. Clojure ile JVM ve Lisp dünyasına ‘Merhaba Dünya’
  2. Clojure’da Basit Veri Türleri

Daha önce basit veri türlerini otopsi altına yatırmıştık, şimdi ise sıra biraz daha karmaşık veri türlerinde…

Clojure’ın yazım dili(syntax) özelliklerini Lisp’ten miras aldığı gibi code-as-data yani veri olarak kod yapısını benimser. Bunu başarmak için aşağıda göstereceğim map ve vektor gibi veri yapılarını kullanır.

Clojure’da veri yapıları, Clojure dilinin fonksiyonel bir dil olması neticesinde tümü değiştirilemez(immutable) ve kalıcı(persistent)’dir. Bu yapılarda bir değişiklik yapmamız gerektiğinde modifiye edilmiş bir kopyalarını alır ve kopyalar üzerinde çalışırız. Clojure veri yapıları - bunlara koleksiyon veri türleri de diyebiliriz - bazı ortak özellikler taşır. Koleksiyonda bulunan elemanların sayısını alabilmek için count, ekleme yapabilmek için conj ve koleksiyon üzerinde baştan sonra işlem yapabilmek için seq gibi metotlardan yardım alırız. Bu metotlar farklı tip veri yapılarında küçük davranış farklılıkları gösterebilir.

Deneylere başlamak için repl’i açalım: lein repl komutunu girelim. REPL’den gelen yanıtları ; => ifadesi ile göstermeye devam edeceğim.

List

Listeler sıralı değerlerden oluşan bir koleksiyondur. Tanımlamak için list fonksiyonu kullanılır.

(list 1 2 3 4)
; => (1 2 3 4)

Görüldüğü gibi fonksiyon tanımlarında olduğu gibi parantezler kullanılıyor. Deneylerimize biraz daha davam edelim.

(def a (list 1 2 3 4))
; => #'user/a
(def b '(1 2 3 4))
; => #'user/b
(= a b)
; => true

Burada a ve b isimlerinde iki liste tanımladık. a listesi list fonksiyonu ile b fonksiyonu ise parantez ifade ile tanımlandı. b listesinin önündeki tek tırnak(‘), Clojure’un liste parantezinin fonksiyon parantezi ile karıştırılmasını engellemek için kullanıldı. Eşitlik testinde ise bire bir aynı oldukları için true değeri döndürdü.

(def c '(4 3 2 1))
; => #'user/c
(= a c)
; => false

Görüyoruz ki list veri yapısında sıralama önemli ve a ile c birbirlerine eşit değiller.

(def d '(4 4 3 1))
; => #'user/d
d
; => (4 4 3 1)

d listesi ise bize çok önemli bir ipucu veriyor. İpucu ise listemizde birden fazla aynı değerin bulunabilmesi.

Aslında dikkat ederseniz Clojure’da list diğer dillerde kullanılan Array veri yapısından başka bir şey değil. Array veri yapısı basit olarak bir indeks ve değer ikililerinden oluşan basit veri yapılarıdır. a ve b listelerini hatırlayalım.

(nth a 0)
; => 1
(nth a 2)
; => 3

Yine görüldüğü üzere nth fonksiyonu, diğer birçok dilden bildiğimiz indexOf’dan çok da farklı birşey değil.

Son olarak list veri yapısının Clojure’da nasıl bir değişken türünde tutulduğunu öğrenmek istersek:

(type a)
; => clojure.lang.PersistentList

Listelere Stack veri yapısı gibi davranmak

List veri yapısının aslında bir Array veri yapısını olduğunu öğrendik. Clojure’da istersek listelere Stack veri yapısı olarak davranabiliriz.

Stack(Yığın) Veri Yapısı: Stack(yığın), bilgisayar bilimlerinde, elemanlarına sadece bir tarafından erişilebilen bir veri yapısıdır. Elemanların erişildiği tarafına üst denir. Bunu üstüste dizilmiş kutulardan oluşan bir yığın gibi düşünebiliriz. Yeni kutu eklemek istediğimizde yığının en üstüne koyarız, yığından kutu almak istediğimizde ise yığının en üstündeki kutuyu alırız.

Stack veri yapısını Clojure ile gerçekleştirmek için iki fonksiyon kullanırız. Bunlar peek ve pop.

peek: Yığının en üstündeki kutuyu almak için kullanılır. Geriye alınan kutunun döndürür.

(peek a)
; => 1

Görüldüğü gibi (1 2 3 4) listesinden 1 değerini döndürdü.

pop: Yığının en üstündeki değeri atarak yığının kalan elemanlarından oluşan veriyi döndürür.

(pop a)
; => (2 3 4)

Vector

Vektörler de listeler gibi bir koleksiyon türüdür. Listelerden biraz daha gelişmiş özellikleri barındırırlar. Yazımın giriş bölümünde tüm koleksiyon türlerinin ortak bir özellik olarak, koleksiyonun tüm öğeleri üzerinde işlem yapmamızı yarayan seq fonksiyonunu desteklediklerini söylemiştim. rseq ise seq fonksiyonu ile aynı işlemi tersten yapar. reverse seq olarak da düşünebiliriz. Vektör tanımlamak için vector fonksiyonu kullanılır. Listelerden bir diğer farkı ise parantez(()) yerine köşeli parantezler([]) ile ifade edilirler.

(def a (vector 1 2 3 4))
; => #'user/a
(def b [1 2 3 4])
; => #'user/b
(def c [4 3 2 1])
; => #'user/c
(= a c)
; => false
(def d [4 4 2 1])
; => #'user/d
d
; => [4 4 2 1]

Buradan çıkaracağımız sonuçlar. Vektörler için de listelerde olduğu gibi sıralama önemli ve bir vektörde aynı değerden birden fazla aynı değer olabilir.

Şimdi başka bir deney yapalım. Bir e listesi tanımlayalım.

(def e '(1 2 3 4))
; => #'user/e
(= a e)
; => true
(= c e)
; => false

Gördüğünüz gibi vektör ve liste yapıları birbirlerine oldukça benzediğinden eşitlik testinde sıralama ve değerleri tuttuğunda true değeri verebiliyorlar. Java’dan miras gelen hashCode metodu ile değerlerini kontrol edelim.

(.hashCode a)
; => 955331
(.hashCode c)
; => 1045631
(.hashCode e)
; => 955331

a vektörü ve e listesinin hashCode değerleri birbirlerine eşit. Bu da bize arkaplanda işlerin nasıl yürüdüğüne yönelik bir fikir veriyor olmalı.

Map

Map’lar da vektörler gibi bir koleksiyon türüdür. İndex değeri ve bir değer tutmak yerine bir anahtar değer ve veri değeri tutarlar. Bilgisayar bilimlerindeki sözlük(dictionary) veri yapısının bir temsilidirler. Maplar temel olarak ikiye ayrılırlar. Bunlar sorted(sıralı) ve hashed(karışık) maplardır. Karışık mapların anahtar değerleri temel olarak Java’dan gelen hashCode ve equals metotlarını desteklemelidir. Sıralı maplarda ise anahtar değerler bir Java Comparable implementasyonu olmalıdır.

hash-map

Karışık mapların kullanımını daha iyi anlamak için aşağıdaki örneğe dikkat edin.

(def a (hash-map :tr 'Türkçe, :en 'İngilizce))
; => #'user/a
(get a :tr)
; => Türkçe

Anahtar-değer çiftleri oluşturmak için virgül(,) kullandık. Anahtarları önceki yazımda bahsettiğim keyword veri türü ile tanımladım.

(def b {:en 'İngilizce, :tr 'Türkçe})
; => #'user/b
(= a b)
; => true

hash-map tanımlamak için küme parantezi ‘{}’ kullanabiliriz. b hash-map’ının farklı sıralamada oluştuğunu ve yine de a ile eşit olduğuna dikkat etmişsinizdir.

(def c {:en 'İngilizce, :tr 'Türkçe, :tr 'Türkçe})
; => IllegalArgumentException Duplicate key: :tr clojure.lang.PersistentArrayMap.createWithCheck (PersistentArrayMap.java:70)

Bu hatadan anladığımız kadarıyla hash-map bize aynı anahtardan birden fazla sayıda sahip olmamıza izin vermiyor.

sorted-map

sorted-map yani sıralı map, Comparable sınıfından türemiş anahtar değerler kullanmalıdır. Bunlar numerik veya string tabanlı değerler olabilir. Tanımlamak için sorted-map fonksiyonu kullanılır.

(def a (sorted-map :a 1, :z 3, :b 2))
; => #'user/a

Burada anahtarlar keyword veri türü ile tanımlandı ve dolayısıyla Comparable’ın bir türevi olmak durumunda. Bunu test etmek için:

(instance? Comparable :a)
; => true

bu şekilde doğrulayabiliriz.

(def b (sorted-map :z 3, :b 2, :a 1))
; => #'user/b
(= a b)
; => true

Peki hash-map ve sorted-map’ı sıralamak istediğimizde ne ile karşılaşırız. c adında bir hash-map tanımlayalım.

(def c (hash-map :a 1, :z 3, :b 2))
; => #'user/c
(= a c)
; => true
(= a b)
; => true
(.hashCode a)
; => -1253234181
(.hashCode b)
; => -1253234181
(.hashCode c)
; => -1253234181

Gördüğünüz gibi hashCode değerleri aynı ve sorted-map a, b ve hash-map c değerleri birbilerine eşit.

Struct-Map

Clojure’da map’ların anahtar-değer ikililerinden oluştuğunu söylemiştik. Bazı durumlarda anahtarları önceden tanımlayıp bunu başka koleksiyonlarla paylaşmamız gerekebilir. Bir başka deyişle Clojure struct-map yapıları, C tipi dillerdeki struct‘lara benzetilebilir. Bu şekilde söyleyince karışık gelmiş olabilir o yüzden basit örnekle pekiştirelim.

(defstruct employee :first-name :last-name :salary)
; => #'user/employee
(def john-doe (struct-map employee :first-name "John" :last-name "Doe" :salary 3000))
; => #'user/john-doe
(def jane-bazz (struct-map employee :first-name "Jane" :last-name "Bazz" :salary 3200))
; => #'user/jane-bazz
(:salary jane-bazz)
; => 3200

Array-Map

Mapların bazen list mantığında olduğu gibi anahtarlarının ayrıca bir indeks değeri tutmaları istenebilir ve böyle durumlarda array-map kullanılır. Array-Map oluşturmak için tahmin edileceği üzere array-map fonksiyonu kullanılır.

Set

Set, matematikten bildiğimiz kümelerden farksızdır. Benzersiz değerlerden oluşur yani liste ve vektörlerdeki gibi aynı değerde birden fazla veri olamaz. Sıralı ve karışık olarak ikiye ayrılırlar.

hash-set

(def a (hash-set :a :b 1 2))
; => #'user/a
(def b (hash-set :b 1 :a 2))
; => #'user/b
user=> (= a b)
; => true

sorted-set

Sorted-set oluşturabilmek küme elemanlarının aynı türden olması gerekir. Aksi halde Clojure .compareTo işlemi gerçekleştirirken hata verecektir.

(def c (sorted-set 1 3 2 4))
; => #'user/c
c
; => #{1 2 3 4}

Set’ler yani kümeler ile bilmemiz gerekenler nelerdir?

1.Her set aynı zamanda fonksiyondur.

(a 1)
; => 1
(a :b)
; => :b

2.clojure.set isim uzayında bulunan küme fonksiyonlarını uygulayabilirsiniz.

(clojure.set/union a c)
; => #{1 4 3 2 :b :a}
(clojure.set/difference a c)
; => #{:b :a}

3.hash-set tanımlamak için #{} yapısını kullanabilirsiniz.

(def my-set #{1 2 3 4})
; => #'user/my-set
(type my-set)
; => clojure.lang.PersistentHashSet

Clojure ile veri yapılarına giriş niteliğinde bir referans yazısı oldu. Bu konu hakkında anlatacak gerçekten çok şey var nasıl anlatacağıma ve ne kadarına anlatacağıma bir fikrim yoktu dolayısıyla buradaki bağlantıdan faydalanarak yazıyı nasıl organize edeceğime karar verdim. Bazı kısımlar ise dandik birer çeviri niteliğinde oldu.

Başka bir yazımda görüşmek üzere. Hoşçakalın.