Bagian 6: Struktur Data Koleksi
  |   Reading time: 23 minute(s).
Kita sudah mengenal tipe data dasar Clojure di bagian 5. Sekarang kita akan belajar struktur data koleksi yang bisa digunakan untuk menghimpun tipe-tipe data dasar tersebut untuk memecahkan masalah yang lebih kompleks. Ada beberapa struktur data koleksi (setelah ini akan disebut sebagai koleksi saja) di Clojure, antara lain list, vector, set, dan map. Keempat struktur data tersebut mempunyai abstraksi yang sama: sequence. Maksud dari abstraksi sequence adalah keempat koleksi tersebut sama-sama “sesuatu yang dirangkai”. Jadi, terlepas dari perbedaan implementasi dan fungsionalitas tiap koleksi, mereka punya karakteristik dan operasi dasar yang sama. Selain itu, tiap koleksi bersifat heterogen, yang berarti mereka bisa menyimpan elemen dengan bermacam-macam tipe data dalam satu koleksi. Di bagian ini, kita akan membahas implementasi konkret dari tiap koleksi (list, vector, set, dan map) dahulu sebelum masuk ke abstraksi sequence. Di tiap koleksi, saya akan memberikan contoh operasi dasar yang bisa dilakukan terhadap mereka.
Immutability dan Persistence
Sebelum melangkah lebih jauh, ada yang perlu diingat lagi tentang data di Clojure, yaitu mereka bersifat immutable, yang berarti kita tidak bisa mengubah data secara sembarangan. Selain itu, mereka juga bersifat persistent, yang berarti nilai lama dari suatu data akan tetap disimpan sebagai bagian dari historical records atau catatan historis.
Mari kita buat sebuah analogi agar lebih mudah dipahami. Misal kita ingin menyimpan data tinggi suatu pohon dalam meter. Di bahasa pemrograman lain tanpa immutability, sebuah nilai disimpan dalam sebuah boks yang kita beri nama Tinggi
agar mudah diakses. Jika kita ingin mengubah nilai Tinggi
, nilai di dalam boks tersebut harus diganti dengan nilai baru. Ini yang dinamakan mutable data; data yang ada di satu boks tersebut bisa diganti-ganti. Data yang sudah diganti akan hilang dan tidak ada catatan historis dari data kita. Selain itu, nama Tinggi
sudah menempel dengan boks, atau dengan kata lain, identitas/nama dan nilai adalah satu kesatuan.
Clojure punya model yang berbeda. Identitas/nama dan nilai dipisah, yang berarti nama Tinggi
menunjuk (bukan menempel seperti contoh di atas) ke lokasi memori yang menyimpan nilai. Nama Tinggi
ini akan menunjuk ke lokasi memori lain jika nilainya perlu diperbarui. Atau kalau pakai istilah Clojure, kita rebind (mengikat/mengaitkan/mengasosiasikan ulang) identitas/nama ke lokasi yang baru. Dengan begini, apabila ingin mengganti nilai Tinggi
, proses yang terjadi kurang lebih seperti di bawah:
- Data di lokasi awal dibiarkan.
- Data pengganti akan disimpan di lokasi yang baru.
- Nama
Tinggi
menunjuk ke lokasi baru tempat disimpannya data pengganti.
Contoh yang cukup sederhana untuk menggambarkan model immutability dan persistence di Clojure adalah tinggi badan manusia. Di sini, yang menjadi identitas adalah “tinggi badan” dan ia tidak berubah. Namun, nilai dari “tinggi badan” (atau nilai yang ditunjuknya) berubah-ubah. Pada saat kecil, tinggi badan kita mungkin 80 cm, dan saat dewasa tinggi badan kita bertambah menjadi 160 cm misalnya. Jadi, meskipun saat ini tinggi kita adalah 160 cm, bukan berarti kita bisa mengabaikan tinggi badan kita sebelumnya. Saat masih kecil, 80 cm itu adalah tinggi badan yang sebenarnya dan valid pada saat itu. Dengan begini, kita punya semacam “catatan historis” dari data kita. Tidak usah khawatir kalau konsep immutability dan persistence ini cukup susah dipahami, terutama di implementasinya. Intinya adalah kalau kita ingin mengubah nilai suatu variabel, Clojure akan membuat nilai baru dan jika diperlukan menyuruh nama variabel menunjuk ke yang baru ini. Mayoritas operasi/fungsi Clojure untuk koleksi akan mengembalikan koleksi baru.
Dalam konteks koleksi ini, wajar kalau kita khawatir dengan penggunaan memori karena tiap perubahan akan memakan memori. Tidak perlu khawatir karena Clojure sudah dioptimasi agar lebih efisien dalam penggunaan memori dan punya teknik structural sharing. Seperti namanya, structural sharing berarti struktur data baik yang lama maupun yang baru dapat berbagi lokasi memori yang sama. Misalkan kita membuat list A
yang berisi (10 11 12)
, lalu membuat list B
dengan menambahkan satu elemen lagi ke list A
. Maka, structural sharing yang terjadi kurang lebih seperti di gambar berikut:
Oke, setelah sedikit penjelasan tentang immutability dan persistence, mari kita mulai berkenalan dengan koleksi yang ada di Clojure. Jangan lupa untuk membuka REPL kalau ingin mengikuti tutorial ini.
List
Struktur data pertama yang akan kita pelajari adalah list. List adalah struktur data utama di Clojure (serta Lisp pada umumnya) dan dipakai untuk mengumpulkan berbagai macam tipe data dalam satu koleksi. Jadi, ia bersifat heterogen, bukan homogen (tidak masalah kalau ingin menyimpan data homogen).
Di belakang layar, list diimplementasikan sebagai singly linked-list. Singly linked-list ini dibuat menggunakan cons cell, yang terdiri dari 2 slot, slot kanan dan slot kiri seperti pada gambar di bawah.
Kedua slot tersebut bisa digunakan untuk menyimpan sebuah nilai atau menunjuk ke cons cell lain. Sebagai tanda “stop” dari list, cons cell paling akhir harus menunjuk ke nil
atau list kosong '()
. Berikut adalah contoh list dengan satu elemen, yaitu '(1)
.
Masih terkait dengan cons cell, kita bisa membangun list yang lebih panjang dengan fungsi cons
. cons
berasal dari kata construct yang bisa diartikan sebagai “membangun” atau “membuat”. Dalam konteks Clojure dan Lisp, cons
dapat digunakan untuk membuat rangkaian data dalam list. Ia dipakai untuk membuat cons cell baru dan menggabungkannya dengan cons cell lain. Fungsi cons
memiliki sintaksis sebagai berikut:
(cons <elemen> <koleksi>)
Karena diimplementasikan sebagai singly linked-list, operasi penambahan elemen paling efisien adalah penambahan di depan/kepala. Jika kita ingin menyisipkan/mengubah nilai di tengah/paling akhir atau secara acak, prosesnya tidak efisien karena harus mengakses banyak cons cell. Maka dari itu, fungsi cons
akan menambahkan elemen di paling depan.
Berikut adalah contoh membuat list '(1)
dan '(1 2 3)
menggunakan fungsi cons
:
user> (cons 1 nil)
;; => (1)
user> (cons 1 ()) ;; list kosong bisa tanpa tanda petik '
;; => (1)
user> (cons 1 '())
;; => (1)
user> (cons 1 2) ;; argumen kedua harus dalam koleksi atau nil
;; Execution error (IllegalArgumentException) at user/eval7534 (REPL:48).
;; Don't know how to create ISeq from: java.lang.Long
user> (cons 1 '(2))
;; => (1 2)
user> (cons 1 '(2 3))
;; => (1 2 3)
user> (cons 1 (cons 2 (cons 3 nil)))
;; => (1 2 3)
user> (cons '(1 2) '(3 4)) ;; '(1 2) dianggap sebagai satu elemen
;; => ((1 2) 3 4)
user> (cons 0 (cons 1 (cons 2 (cons 3 nil))))
;; => (0 1 2 3)
Representasi list '(0 1 2 3)
di cons cell:
Seringkali kita tidak perlu memakai cons
untuk membuat list karena kita bisa memakai literal list secara langsung seperti '(1 2 3)
atau dengan fungsi list
seperti (list 1 2 3)
. Perlu diingat lagi bahwa list juga bisa dianggap sebagai function call, sehingga kita harus memberikan tanda petik satu '
untuk mendapatkan datanya. Fungsi cons
berguna apabila kita ingin menambah elemen satu per satu ke list yang sudah ada.
;; simpan list dalam my-list agar dapat dipakai berulang kali.
;; my-list dapat menyimpan tipe data yang bervariasi.
user> (def my-list '("satu" 2 3.0 :empat \5 enam))
;; => #'user/my-list
user> my-list
;; => ("satu" 2 3.0 :empat \5 enam)
user> (def my-list-2 (list "satu" 2 3.0 :empat \5 'enam))
;; => #'user/my-list-2
user> my-list-2
;; => ("satu" 2 3.0 :empat \5 enam)
user> (cons 0 my-list) ;; mengembalikan list baru
;; => (0 "satu" 2 3.0 :empat \5 enam)
user> (cons nil my-list)
;; => (nil "satu" 2 3.0 :empat \5 enam)
user> my-list ;; my-list tidak berubah
;; => ("satu" 2 3.0 :empat \5 enam)
Alternatif fungsi cons
adalah fungsi conj
(kependekan dari conjoin yang berarti menggabungkan). Fungsi conj
akan menambahkan elemen secara efisien tergantung dari struktur data yang dipakai. Dalam hal ini, karena list paling efisien kalau elemen ditambahkan di depan, conj
akan bekerja seperti cons
. Nanti kita akan melihat kalau conj
bekerja dengan cara yang berbeda di koleksi lain. Selain itu, cons
hanya bisa menambahkan satu elemen, sedangkan conj
bisa menambahkan banyak elemen sekaligus.
Perlu diperhatikan bahwa urutan argumen di conj
berbeda dengan cons
:
(conj <koleksi> <elemen>)
Tips
Untuk memeriksa dokumentasi dari tiap fungsi, kita bisa memakai fungsi
doc
. Selain itu kita juga bisa melihat source code dari fungsi clojure dengan fungsisource
. Berikut adalah contoh jika kita ingin tahu dokumentasi serta source code dari fungsiconj
. Output dari kedua fungsi tersebut tidak akan dijelaskan di bagian ini.user> (doc conj) ------------------------- clojure.core/conj ([] [coll] [coll x] [coll x & xs]) conj[oin]. Returns a new collection with the xs 'added'. (conj nil item) returns (item). (conj coll) returns coll. (conj) returns []. The 'addition' may happen at different 'places' depending on the concrete type. nil user> (source conj) (def ^{:arglists '([] [coll] [coll x] [coll x & xs]) :doc "conj[oin]. Returns a new collection with the xs 'added'. (conj nil item) returns (item). (conj coll) returns coll. (conj) returns []. The 'addition' may happen at different 'places' depending on the concrete type." :added "1.0" :static true} conj (fn ^:static conj ([] []) ([coll] coll) ([coll x] (clojure.lang.RT/conj coll x)) ([coll x & xs] (if xs (recur (clojure.lang.RT/conj coll x) (first xs) (next xs)) (clojure.lang.RT/conj coll x))))) nil
Selain dengan 2 fungsi di atas, kita juga bisa mengakses laman https://clojuredocs.org/, yang berisi dokumentasi beserta contoh pemakaiannya.
user> (conj my-list 0) ;; perhatikan urutan argumen
;; => (0 "satu" 2 3.0 :empat \5 'enam)
user> (conj my-list 77 88 99 100)
;; => (100 99 88 77 "satu" 2 3.0 :empat \5 'enam)
user> (conj my-list '(77 88 99 100)) ;; '(77 88 99 100) dianggap satu elemen
;; => ((77 88 99 100) "satu" 2 3.0 :empat \5 'enam)
Kita bisa memeriksa apakah suatu koleksi bertipe list dengan fungsi predikat list?
. Selain itu, untuk memeriksa apakah suatu list memiliki elemen atau kosong, pakai fungsi empty?
.
user> (list? nil) ;; nil bukan list
;; => false
user> (list? ())
;; => true
user> (list? '())
;; => true
user> (list? my-list)
;; => true
user> (empty? nil) ;; tapi nil dianggap sebagai sesuatu yang kosong seperti list kosong.
;; => true
user> (empty? '())
;; => true
user> (empty? my-list)
;; => false
fungsi
empty?
bisa dipakai di koleksi lain.
Indeks elemen list dimulai dari 0, sama seperti bahasa pemrograman lain. Untuk mengakses elemen dari suatu list berdasarkan indeksnya, kita bisa memakai fungsi nth
dengan sintaksis:
(nth <koleksi> <indeks>)
Jika kita mencoba akses indeks yang tidak ada, nth
akan error. nth
punya argumen opsional untuk memberi tahu kita kalau elemen tidak ditemukan daripada menghasilkan exception.
user> (nth my-list 0)
;; => "satu"
user> (nth my-list 3)
;; => :empat
user> (nth my-list 10)
;; Execution error (IndexOutOfBoundsException) at user/eval7604 (REPL:144).
;; null
user> (nth my-list 10 :nilai-tidak-ada)
;; => :nilai-tidak-ada
Kita bisa mengambil nilai pertama dari suatu list dengan fungsi first
. Kalau ingin mengambil nilai pertama dari list kosong, first
akan mengembalikan nil
. Pelengkap dari fungsi first
ini adalah rest
, yang mana akan mengembalikan list tanpa nilai pertamanya.
user> (first my-list)
;; => "satu"
user> (rest my-list)
;; => (2 3.0 :empat \5 'enam)
user> (first nil)
;; => nil
user> (first '())
;; => nil
user> (rest '())
;; => ()
user> (rest nil)
;; => ()
Vector
Karena list tidak efisien saat mengakses elemen secara acak, Clojure menyediakan koleksi alternatif yang lebih sering dipakai dibandingkan list, yaitu vector. Vector mirip seperti array dan elemennya bisa diakses secara random dengan efisien. Tiap elemen di vector juga punya indeks dari 0.
Untuk membuat vector, kita bisa memakai literal dengan tanda kurung siku []
atau fungsi vector
. Kita bisa juga mengonversi koleksi lain ke vector dengan fungsi vec
. Fungsi predikat vector adalah vector?
.
user> (def my-vector ["satu" 2 3.0 :empat \5 'enam])
;; => #'user/my-vector
user> my-vector
;; => ["satu" 2 3.0 :empat \5 enam]
user> (def my-vector (vector "satu" 2 3.0 :empat \5 'enam))
;; => #'user/my-vector
user> (def my-vector-2 (vector "satu" 2 3.0 :empat \5 'enam))
;; => #'user/my-vector-2
user> my-vector-2
;; => ["satu" 2 3.0 :empat \5 enam]
user> (vec '(4 5 6))
;; => [4 5 6]
user> (vector? (vec '(4 5 6)))
;; => true
user> (vector? '(4 5 6))
;; => false
Fungsi cons
di vector akan menambah elemen ke bagian depan, namun yang dikembalikan bukan dalam vector melainkan sequence. Sequence ini adalah logical list dan akan dibahas di akhir bagian ini.
user> (cons 0 my-vector)
;; => (0 "satu" 2 3.0 :empat \5 enam) ;; bukan list, tapi sequence
user> (vector? (cons 0 my-vector))
;; => false
user> (list? (cons 0 my-vector))
;; => false
user> (seq? (cons 0 my-vector))
;; => true
Sementara itu, fungsi conj
akan menambah elemen ke bagian belakang vector. Ini karena vector mirip seperti array dan operasi penambahan elemen paling efisien adalah di paling belakang.
user> (conj my-vector 0)
;; => ["satu" 2 3.0 :empat \5 enam 0]
user> (conj my-vector nil)
;; => ["satu" 2 3.0 :empat \5 enam nil]
user> (conj my-vector 7)
;; => ["satu" 2 3.0 :empat \5 enam 7]
user> (conj my-vector 33 44 55)
;; => ["satu" 2 3.0 :empat \5 enam 33 44 55]
user> (conj my-vector '(33 44 55)) ;; bisa menambahkan list ke vector
;; => ["satu" 2 3.0 :empat \5 enam (33 44 55)]
user> (conj my-vector [33 44 55])
;; => ["satu" 2 3.0 :empat \5 enam [33 44 55]]
Untuk mengakses elemen vector berdasarkan indeksnya kita bisa memakai nth
atau get
. Perlu diperhatikan kalau get
tidak dapat bekerja di list. Ini karena get
bekerja untuk struktur data yang punya karakteristik key-value pair (key-value pair akan dijelaskan nanti). Vector dianggap sebagai key-value pair karena indeks vector diperlakukan menjadi key dan nilai di indeks tersebut menjadi value-nya. List tidak bisa bekerja seperti ini maka dari itu fungsi get
tidak bisa diterapkan di list. get
juga menerima argumen opsional yang akan dikembalikan jika tidak ada indeks yang dicari.
user> (nth my-vector 0)
;; => "satu"
user> (nth my-vector 5)
;; => enam
user> (get my-vector 0)
;; => "satu"
user> (get my-vector 4)
;; => \5
user> (get my-vector 99 :tidak-ada)
;; => :tidak-ada
Kita bisa memeriksa apakah suatu vector memiliki elemen di indeks tertentu dengan fungsi contains?
. Fungsi tersebut akan mengecek indeks, bukan nilai di indeks yang dimaksud. contains?
juga tidak bisa dipakai di list.
user> (contains? my-vector 0) ;; my-vector punya nilai di indeks 0?
;; => true
user> (contains? my-vector 4)
;; => true
user> (contains? my-vector 99) ;; my-vector punya nilai di indeks 99?
;; => false
user> (contains? my-list 0)
;; Execution error (IllegalArgumentException) at user/eval7662 (REPL:220).
;; contains? not supported on type: clojure.lang.PersistentList
Untuk mengubah elemen suatu vector, fungsi assoc
(kependekan dari associate) dapat digunakan. Selain itu, assoc
juga bisa digunakan untuk menambah elemen, namun terbatas di indeks n+1
di mana n
adalah indeks terbesar saat ini.
Fungsi assoc
memiliki sintaksis sebagai berikut:
(assoc <koleksi> <indeks> <nilai>)
user> (assoc my-vector 2 :DIGANTI)
;; => ["satu" 2 :DIGANTI :empat \5 enam]
user> (assoc my-vector 7 :DIGANTI) ;; indeks terbesar adalah 5
;; Execution error (IndexOutOfBoundsException) at user/eval7668 (REPL:229).
;; null
user> (assoc my-vector 6 :DIGANTI)
;; => ["satu" 2 3.0 :empat \5 enam :DIGANTI]
Seringkali kita menemui kasus di mana yang kita butuhkan hanya sebagian elemen saja, bukan keseluruhan vector. Vector parsial ini disebut sebagai subvector dan cara mendapatkannya bisa dengan fungsi subvec
. Fungsi subvec
akan memotong vector berdasarkan indeks yang kita masukkan sebagai titik awal subvector (dan titik akhir jika diperlukan).
user> (subvec my-vector 3) ;; potong mulai dari indeks 3
;; => [:empat \5 enam]
user> (subvec my-vector 2 4) ;;
;; => [3.0 :empat]
Set
Set adalah koleksi dari nilai-nilai yang unik. Jika dalam suatu set terdapat duplikasi nilai, maka hanya ada satu nilai yang disimpan. Ini sama seperti konsep himpunan di matematika. Literal set menggunakan #{}
, contohnya adalah #{1 2 3}
. Kita bisa membuat set dengan fungsi hash-set
atau sorted-set
. Perbedaan dari kedua fungsi tersebut adalah sorted-set
dapat mengurutkan nilai dalam set. Fungsi predikatnya adalah set?
.
;; literal set harus tanpa duplikasi, jika tidak akan error.
user> (def my-set #{1 2 1 3 4 5 5})
;; Syntax error reading source at (REPL:107:35).
;; Duplicate key: 1
user> (def my-set #{1 2 3 4 5})
;; => #'user/my-set
user> my-set
;; => #{1 4 3 2 5} ;; urutan seringkali tidak sesuai dengan yang kita masukkan di awal
user> (hash-set 1 2 3 4 2 5 1) ;; duplikat akan diabaikan
;; => #{1 4 3 2 5}
user> (sorted-set 4 2 4 5 3 1 2)
;; => #{1 2 3 4 5}
user> (set? my-set)
;; => true
user> (set? my-list)
;; => false
Kita bisa mengonversi struktur koleksi lain ke set dengan fungsi set
. Jika di koleksi lain ada duplikasi, nilai duplikat juga akan diabaikan.
user> (set my-vector)
;; => #{3.0 enam "satu" 2 \5 :empat}
user> (set my-list)
;; => #{3.0 'enam "satu" 2 \5 :empat}
user> (set [1 2 3 1 2 3])
;; => #{1 3 2}
user> (set '(1 2 3 1 2 3))
;; => #{1 3 2}
Berbeda dengan fungsi contains?
di vector yang memeriksa indeks, fungsi contains?
di set benar-benar memeriksa apakah suatu nilai ada di set.
user> (contains? my-set 3)
;; => true
user> (contains? my-set 0)
;; => false
user> (contains? my-set 99)
;; => false
Fungsi get
di set bisa dipakai untuk mendapatkan nilai yang ada di set. Set bisa dianggap sebagai key-value pair, di mana elemen dalam set dianggap sebagai key sekaligus value. Sehingga, nilai kembaliannya adalah elemen itu sendiri (jika ada di set).
user> (get my-set 0)
;; => nil
user> (get my-set 1)
;; => 1
user> (get my-set 4)
;; => 4
user> (get my-set 42)
;; => nil
Karena set berisi nilai yang unik, operasi pembaruan yang mungkin dilakukan adalah antara menambah elemen baru (yang unik) atau menghapus elemen yang sudah ada.
Untuk menambah elemen pada set, gunakan fungsi conj
.
user> (conj my-set 42)
;; => #{1 4 3 2 5 42}
user> (conj my-set 88 77 66 66)
;; => #{1 88 4 77 3 2 66 5}
user> (conj my-set 42 42 84 nil)
;; => #{nil 1 4 3 2 5 42 84}
Fungsi disj
(yang merupakan kependekan dari disjoin yang berarti memisahkan) dapat dipakai untuk menghapus elemen dari set.
user> (disj my-set 4)
;; => #{1 3 2 5}
user> (disj my-set 1 2 4 9)
;; => #{3 5}
user> (disj my-set 29)
;; => #{1 4 3 2 5}
Map
Map adalah struktur data koleksi untuk menyimpan nilai dalam bentuk key-value pair. Pair ini digunakan untuk mengasosiasikan 2 nilai, contohnya asosiasi nama orang dan tinggi badan mereka. Dalam Bahasa Indonesia, map lebih tepat diterjemahkan sebagai pemetaan, dalam hal ini pemetaan satu nilai ke nilai yang lain (seperti contoh asosiasi di atas). Map sangat sering digunakan di Clojure karena ia memudahkan kita untuk memodelkan masalah yang kita selesaikan. Nantinya kita juga akan sering bertemu dengannya. Literal map memakai kurung kurawal {}
(perhatikan perbedaannya dengan set yang punya #
di depannya). Contoh sederhana map adalah {"ani" 167}
yang mengasosiasikan nilai "ani"
dengan nilai 167
. key dalam suatu map ("ani"
untuk map di atas) bisa dalam semua tipe data tapi harus unik. Fungsi hash-map
dan sorted-map
juga dapat dipakai untuk membuat map. Seperti pada set, perbedaan kedua fungsi tersebut adalah sorted-map
bisa mengembalikan map yang sudah terurut.
;; koma opsional tapi sangat membantu untuk membedakan tiap asosiasi
user> (def tinggi-badan {"ani" 167, "budi" 163, "catur" 172})
;; => #'user/tinggi-badan
user> tinggi-badan
;; => {"ani" 167, "budi" 163, "catur" 172}
user> (hash-map "budi" 163 "catur" 172 "ani" 167)
;; => {"catur" 172, "budi" 163, "ani" 167}
user> (sorted-map "budi" 163 "catur" 172 "ani" 167)
;; => {"ani" 167, "budi" 163, "catur" 172}
Fungs predikat untuk map adalah map?
. Perlu saya informasikan juga bahwa ada fungsi map
di Clojure. Kalau melihat dari pola di koleksi sebelumnya, fungsi ini seharusnya untuk mengonversi koleksi lain ke map. Sayangnya tidak seperti itu. Fungsi map
adalah untuk memetakan suatu fungsi ke tiap elemen suatu koleksi. Kita akan bertemu fungsi map
saat kita membahas pemrograman fungsional.
user> (map? my-set)
;; => false
user> (map? tinggi-badan-key)
;; => true
user> (doc map)
;; -------------------------
;; clojure.core/map
;; ([f] [f coll] [f c1 c2] [f c1 c2 c3] [f c1 c2 c3 & colls])
;; Returns a lazy sequence consisting of the result of applying f to
;; the set of first items of each coll, followed by applying f to the
;; set of second items in each coll, until any one of the colls is
;; exhausted. Any remaining items in other colls are ignored. Function
;; f should accept number-of-colls arguments. Returns a transducer when
;; no collection is provided.
;; nil
Di clojure, keyword sangat umum dipakai sebagai key sebuah map. Oleh karena itu, contoh di atas jika memakai keyword akan menjadi:
user> (def tinggi-badan-key {:ani 167, :budi 163, :catur 172})
;; => #'user/tinggi-badan-key
user> tinggi-badan-key
;; => {:ani 167, :budi 163, :catur 172}
Untuk mendapatkan nilai yang terasosiasi dengan suatu key bisa memakai get
. Keyword itu sendiri juga bisa berperilaku seperti sebuah operator atau fungsi. Contoh di bawah ini bisa menjelaskan apa yang saya maksud dengan keyword sebagai operator/fungsi.
user> (get tinggi-badan "catur")
;; => 172
user> (get tinggi-badan-key :budi)
;; => 163
user> (get tinggi-badan-key :dina)
;; => nil
user> (get tinggi-badan-key :dina :tidak-ada)
;; => :tidak-ada
user> (:ani tinggi-badan-key) ;; keyword sebagai fungsi
;; => 167
Kadang kita ingin mendapatkan key saja atau nilainya saja yang ada di map. Fungsi keys
dan vals
bisa membantu untuk mendapatkannya.
user> (vals tinggi-badan-key)
;; => (167 163 172) ;; sequence, bukan list
user> (keys tinggi-badan-key)
;; => (:ani :budi :catur) ;; sequence, bukan list
user> (keys tinggi-badan)
;; => ("ani" "budi" "catur") ;; sequence, bukan list
user> (list? (keys tinggi-badan-key))
;; => false
user> (seq? (keys tinggi-badan-key))
;; => true
Kita bisa menambahkan key-value pair baru dalam map dengan fungsi assoc
. Selain itu, ia juga bisa untuk mengganti nilai yang sudah ada di map.
user> (assoc tinggi-badan-key :dina 164)
;; => {:ani 167, :budi 163, :catur 172, :dina 164}
user> (assoc tinggi-badan-key :ani 169)
;; => {:ani 169, :budi 163, :catur 172}
Dengan fungsi update
, kita bisa memperbarui nilai berdasarkan dengan fungsi lain. Misalnya tinggi Budi bertambah 1 cm dan kita ingin memperbarui tinggi Budi di map. Fungsi inc
(increment) akan menambah 1 ke argumennya, dan ia bisa dipakai untuk mengubah tinggi Budi dengan fungsi update
.
user> (update tinggi-badan-key :budi inc)
;; => {:ani 167, :budi 164, :catur 172}
Selanjutnya, untuk menghapus key-value pair di map, gunakan fungsi dissoc
. Fungsi dissoc
(dissociate) adalah kebalikan dari fungsi assoc
. Jika key yang ingin kita hapus tidak ada di map, maka ia akan diabaikan tanpa memunculkan error.
user> (dissoc tinggi-badan-key :catur :ani)
;; => {:budi 163}
user> (dissoc tinggi-badan-key :fina)
;; => {:ani 167, :budi 163, :catur 172}
Abstraksi Sequence
Sekarang kita masuk ke pembahasan sequence. Sequence adalah abstraksi untuk sesuatu yang bisa dirangkai. List, vector, map, dan set adalah beberapa cara untuk merangkai dan mengumpulkan data. Dengan abstraksi sequence, kita bisa memperlakukan keempat struktur data tersebut sebagai sesuatu yang sama, yaitu sama-sama “rangkaian” data.
Sequence ditampilkan mirip seperti list biasa dengan tanda kurung dan seringkali orang menyangka sequence adalah list, tapi sebenarnya ia berbeda dengan list. Sequence adalah logical list dan punya implementasi yang berbeda dengan list. List biasa diimplementasikan dalam cons cell, sedangkan sequence diimplementasikan dalam ISeq
interface yang memungkinkan kita untuk mengakses elemen dari berbagai macam struktur data koleksi.
Di contoh-contoh sebelumnya kita sudah melihat ada beberapa fungsi yang mengembalikan sequence. Mayoritas fungsi Clojure yang berhubungan dengan koleksi memang akan mengonversi suatu koleksi menjadi sequence agar bisa diproses. Proses konversi ini terjadi karena kebanyakan algoritma dan fungsi di Clojure didefinisikan berdasarkan sequence. Setelah diproses, sequence dapat dikonversi lagi ke struktur data awal jika diperlukan.
Seperti koleksi lain, sequence juga bersifat immutable dan persistent. Kita bisa mengonversi suatu koleksi ke sequence dengan fungsi seq
. Selain itu, kita juga bisa memeriksa apakah suatu koleksi dapat dikonversi menjadi sequence dengan fungsi seqable?
. Karena list dan sequence ditampilkan dengan tanda kurung, fungsi predikat seq?
dapat membantu kita untuk membedakan mereka.
user> (seq my-list)
;; => ("satu" 2 3.0 :empat \5 enam)
user> (seq my-vector)
;; => ("satu" 2 3.0 :empat \5 enam)
user> (seq my-set)
;; => (1 4 3 2 5)
user> (seq tinggi-badan-key)
;; => ([:ani 167] [:budi 163] [:catur 172])
user> (seqable? my-vector)
;; => true
user> (seqable? "test") ;; string dianggap sebagai rangkaian dari character
;; => true
user> (seqable? 3)
;; => false
user> (seqable? :tidak-bisa)
;; => false
user> (seq? my-list) ;; literal list dianggap sebagai sequence
;; => true
user> (seq? my-vector)
;; => false
user> (seq? (seq my-set))
;; => true
Sequence punya 3 operasi dasar yang bisa dipakai untuk semua struktur data koleksi yang menerapkan abstraksi sequence, yaitu first
, rest
, dan cons
yang mana sudah kita temui di contoh-contoh sebelumnya. Kebanyakan fungsi untuk memroses koleksi didasarkan pada ketiga fungsi tersebut.
-
first
Fungsi ini mengembalikan nilai pertama dari suatu koleksi. Jika koleksi tersebut kosong,
nil
akan dikembalikan.user> (first my-list) ;; => "satu" user> (first my-vector) ;; => "satu" user> (first my-set) ;; => 1 user> (first tinggi-badan-key) ;; => [:ani 167] user> (first '()) ;; => nil user> (first []) ;; => nil user> (first #{}) ;; => nil user> (first {}) ;; => nil
-
rest
Fungsi ini adalah pelengkap dari
first
. Ia akan mengembalikan sequence tanpa nilai pertamanya. Dengan fungsi predikat yang sudah dipelajari, kita bisa memastikan bahwa yang dikembalikan adalah sequence, bukan list. Jika tidak ada nilai lagi dalam koleksi, ia akan mengembalikan()
.user> (rest my-list) ;; => (2 3.0 :empat \5 enam) user> (rest my-vector) ;; => (2 3.0 :empat \5 enam) user> (rest my-set) ;; => (4 3 2 5) user> (rest tinggi-badan-key) ;; => ([:budi 163] [:catur 172]) user> (rest '()) ;; => () user> (rest []) ;; => () user> (rest #{}) ;; => () user> (rest {}) ;; => () user> (list? (rest my-vector)) ;; => false user> (seq? (rest my-vector)) ;; => true user> (type (rest my-vector)) ;; => clojure.lang.PersistentVector$ChunkedSeq
-
cons
Di atas saya sudah menjelaskan fungsi
cons
(besertafirst
danrest
) dalam konteks list. Saya juga sempat memberikan contoh fungsicons
untuk vector yang ternyata nilai kembaliannya adalah sequence, bukan vector. Jika kita buka dokumentasi fungsicons
, akan semakin jelas bahwa ia bekerja dalam konteks sequence.user> (doc cons) ;; ------------------------- ;; clojure.core/cons ;; ([x seq]) ;; Returns a new seq where x is the first element and seq is ;; the rest. ;; nil
Fungsi
cons
dapat bekerja di banyak koleksi dan akan mengembalikan sequence.user> (cons 99 my-list) ;; => (99 "satu" 2 3.0 :empat \5 enam) user> (cons 99 my-vector) ;; => (99 "satu" 2 3.0 :empat \5 enam) user> (cons 99 my-set) ;; => (99 1 4 3 2 5) user> (cons 99 tinggi-badan-key) ;; karena sudah dalam sequence, kita bisa masukkan nilai tunggal. ;; => (99 [:ani 167] [:budi 163] [:catur 172]) user> (cons [:dina 165] tinggi-badan-key) ;; => ([:dina 165] [:ani 167] [:budi 163] [:catur 172]) user> (seq? (cons [:dina 165] tinggi-badan-key)) ;; => true
Ada satu hal lagi yang ingin saya sampaikan terkait sequence, yaitu mereka lazy. Maksudnya adalah Clojure tidak akan mengevaluasi sequence sampai benar-benar dibutuhkan. Dengan karakteristik ini, kita bisa membuat sebuah sequence dengan panjang tak terbatas (sampai infinity) tanpa harus menunggu Clojure mendapatkan semua elemennya (karena tidak perlu). Kita akan bertemu lagi dengan lazy sequence nanti. Untuk sekarang, itu saja yang perlu diketahui.
Penutup
Di bagian yang cukup panjang ini, kita sudah belajar struktur data koleksi Clojure yang bisa digunakan untuk menghimpun data, mulai dari list, vector, set, lalu map. Kita juga sudah melihat bahwa Clojure punya abstraksi sequence yang memungkinkan kita untuk memperlakukan semua koleksi sebagai entitas yang sama, yaitu sequence. Di belakang layar, kebanyakan fungsi Clojure bekerja berdasarkan sequence. Selain itu, kita sudah mengenal 3 operasi dasar sequence yaitu first
, rest
, dan cons
.
Di bagian selanjutnya, kita akan berkenalan dengan percabangan dan cara mengontrol alur kerja program Clojure kita.