Apa itu Clojure?

Mengenal Clojure sebagai Lisp fungsional di atas JVM yang berorientasi data dan bertipe dinamis.

Daftar isi

Topik ini memperkenalkan Clojure sebagai bahasa pemrograman: posisinya sebagai dialek Lisp fungsional di atas JVM, pendekatannya yang berorientasi data, dan sifat tipe dinamisnya. Bagian akhir topik memperlihatkan satu program kecil yang mengolah data dengan map, filter, dan reduce lengkap dengan keluaran aslinya, sehingga istilah-istilah inti yang digunakan sepanjang jalur memiliki gambaran konkret sejak awal.

Clojure sebagai Lisp di atas JVM#

Clojure adalah bahasa pemrograman fungsional yang ditulis sebagai dialek Lisp dan dieksekusi di atas JVM (Java Virtual Machine), mesin virtual yang menjalankan program Java. Kode Clojure dikompilasi menjadi bytecode, kode menengah yang dipahami JVM, sehingga program Clojure berjalan dengan kecepatan yang setara program Java. Melalui interop, mekanisme pemanggilan antarbahasa dari bahasa host, program Clojure juga dapat memanggil kelas serta pustaka Java secara langsung. Karena artefak akhirnya berupa bytecode, program Clojure dikemas dan dijalankan dengan perkakas Java yang biasa, mulai dari berkas JAR sampai JVM yang sudah berjalan pada server. Bahasa yang mengandalkan lingkungan eksekusi milik bahasa lain seperti ini disebut hosted language, dan untuk Clojure host-nya adalah JVM. Clojure dirilis pertama kali pada 2007 oleh Rich Hickey dan terus dikembangkan sebagai proyek open source. Warisan Lispnya tetap terlihat pada dua hal: kode ditulis dalam bentuk daftar berbatas kurung, dan kode dapat diperlakukan sebagai data, sifat yang menjadi fondasi sistem macro.

Data immutable sebagai default#

Nilai koleksi pada Clojure bersifat immutable, artinya tidak dapat diubah setelah dibuat. Operasi yang tampak seperti perubahan, misalnya menambah elemen pada vector, sebenarnya menghasilkan koleksi baru, sedangkan koleksi lama tetap utuh dan dapat digunakan seperti semula. Konsekuensinya, sebuah nilai boleh dikirim ke bagian mana pun dari program tanpa risiko berubah, dan aliran data lebih mudah dianalisis karena tidak ada perubahan yang harus dilacak. Pada bahasa yang memungkinkan perubahan di tempat, dua nama yang menunjuk satu daftar yang sama dapat memunculkan perilaku yang mengejutkan karena satu sisi mengubah isi yang sedang digunakan sisi lain. Pada Clojure, situasi itu tidak mungkin terjadi karena isi tidak pernah berubah, dan yang berbeda hanyalah nama mana yang menunjuk versi mana. Untuk menjaga performa, Clojure menerapkan persistent data structure, struktur data yang membuat versi baru dengan menggunakan ulang sebagian besar simpul lamanya, teknik yang disebut structural sharing. Salinan baru pun tidak menyalin seluruh isi, sehingga immutability tidak berarti boros memori.

Map dan vector sebagai medium utama#

Clojure berorientasi data: bagian-bagian program berkomunikasi melalui data biasa yang diolah fungsi, bukan melalui jaringan objek yang menyembunyikan keadaannya. Dua struktur yang paling sering muncul adalah map, kumpulan pasangan kunci-nilai, dan vector, kumpulan berurutan yang diakses lewat indeks. Data nyata seperti siswa, pengukuran, atau pesanan direpresentasikan sebagai map, sedangkan sekumpulan data sejenis ditampung dalam vector. Keyword, penanda berawalan titik dua seperti :nama, digunakan sebagai kunci map secara konvensi karena identitasnya ditentukan oleh namanya sendiri, sehingga dua keyword dengan nama sama dapat dibandingkan dengan cepat. Bentuk literalnya sama dengan notasi data itu sendiri, misalnya {:nama "Ali" :skor 82} untuk satu siswa dan [82 74 91 66] untuk empat skor. Fungsi bawaan seperti map, filter, dan reduce bekerja atas koleksi apa pun karena seluruh koleksi mengikuti kontrak yang sama, sehingga satu fungsi melayani banyak bentuk data. Pendekatan ini memangkas kebutuhan mendefinisikan kelas untuk setiap bentuk data, kebiasaan yang lazim pada pemrograman berorientasi objek. Pada Clojure, skema data melekat pada datanya sendiri.

Tipe dinamis#

Clojure bertipe dinamis: pemeriksaan tipe dilakukan ketika form dievaluasi, dan tipe menempel pada nilai, bukan pada nama. Satu nama boleh menampung vector pada satu saat dan map pada saat lain, sedangkan pemeriksaan kesesuaian terjadi pada nilai yang benar-benar mengalir. Konsekuensi praktisnya, penulisan kode bergerak cepat karena tidak ada deklarasi tipe yang harus dijaga, tetapi kesalahan bentuk data, misalnya salah ketik keyword, baru terlihat ketika form itu dievaluasi. REPL (read-eval-print loop), proses interaktif yang membaca form lalu mencetak hasilnya, membantu menemukan kesalahan tersebut seketika, dan pengujian menutup bagian yang tidak terjangkau evaluasi manual. Apabila pemeriksaan yang lebih ketat diperlukan, deskripsi bentuk data dapat ditambahkan melalui spec tanpa mengubah sifat tipe bahasanya. Kombinasi tipe dinamis dan data immutable inilah yang membuat siklus eksperimen singkat: satu form ditulis, satu nilai diamati, lalu form berikutnya disusun dari nilai yang sudah terbukti.

Sintaks s-expression secara ringkas#

Seluruh kode Clojure ditulis sebagai s-expression, ekspresi berbentuk daftar berbatas kurung yang menempatkan operator pada posisi pertama. Penjumlahan dua bilangan ditulis (+ 2 3), bukan 2 + 3, dan bentuk bersarang mengikuti pola yang sama, misalnya (* (+ 1 2) 4). Setiap s-expression disebut form, satuan terkecil yang dievaluasi dan menghasilkan satu nilai. Karena operator selalu berada di depan, urutan evaluasi tidak bergantung pada tabel prioritas operator, dan tanda kurung menunjukkan struktur secara eksplisit. Tanda kurung pada Clojure memang terlihat lebih padat daripada kata kunci bahasa lain, tetapi strukturnya konsisten: elemen dipisah spasi, dan tingkat sarang terbaca dari pasangan kurungnya. Membaca (* (+ 1 2) 4) dari luar ke dalam menghasilkan urutan yang jelas, yaitu jumlahkan satu dan dua terlebih dahulu, lalu kalikan hasilnya dengan empat. Konsistensi ini membuat satu aturan pembacaan berlaku untuk seluruh bahasa, dari ekspresi aritmetika sampai definisi fungsi. Kode berbentuk daftar bersarang ini sekaligus data, yaitu daftar simbol, sehingga kode dapat dibaca, diubah, dan dibuat oleh kode lain. Kemampuan itulah yang digunakan sistem macro.

Dua form berikut disajikan sebagaimana diketik pada REPL. Penanda tidak-dijalankan digunakan karena mode skrip tidak mencetak nilai form, sedangkan nilai yang dicetak REPL ditampilkan pada blok kedua.

clojure Dua form dan bentuk s-expression-nya
; tidak-dijalankan
; dievaluasi di REPL, yang selalu mencetak nilai setiap form
(+ 2 3)
(* (+ 1 2) 4)
text Nilai yang dicetak REPL untuk kedua form
5
12

Program kecil: statistik nilai ujian#

Sifat-sifat di atas bekerja bersama pada program kecil berikut. Data berupa vector berisi empat map siswa, kemudian diolah dengan mapv, filter, dan reduce untuk menghasilkan daftar skor, daftar siswa yang lulus, total skor, dan rata-rata kelas. Berkas nilai-uas.clj disimpan apa adanya di dalam topik ini dan dapat dijalankan dengan clojure -M nilai-uas.clj pada lingkungan yang sudah terpasang.

clojure nilai-uas.clj
(def nilai-uas
  [{:nama "Ali" :skor 82}
   {:nama "Budi" :skor 74}
   {:nama "Citra" :skor 91}
   {:nama "Dewi" :skor 66}])

(println "semua skor      :" (mapv :skor nilai-uas))
(println "yang lulus      :" (mapv :nama (filter #(>= (:skor %) 75) nilai-uas)))
(println "total skor kelas:" (reduce + (map :skor nilai-uas)))
(println "rata-rata kelas :" (/ (reduce + (map :skor nilai-uas)) (count nilai-uas)))
text Keluaran
semua skor      : [82 74 91 66]
yang lulus      : [Ali Citra]
total skor kelas: 313
rata-rata kelas : 313/4

Baris terakhir keluaran berupa 313/4, bukan 78.25, karena pembagian bilangan bulat pada Clojure menghasilkan Ratio, pecahan eksak yang dicetak apa adanya. Bila bentuk desimal diinginkan, rasio itu dikonversi dengan fungsi double. Baris lain membaca aliran data dari kiri ke kanan. mapv :skor mengambil nilai keyword :skor dari setiap map dan mengembalikan hasilnya sebagai vector. filter menyaring siswa dengan skor minimal 75 melalui fungsi anonim #(>= (:skor %) 75), sedangkan reduce + menjumlahkan seluruh skor menjadi satu nilai. Tidak ada satu pun baris yang mengubah nilai-uas. Setiap operasi membuat koleksi baru dari koleksi yang lama.

Kesalahan umum#

Tiga kekeliruan pertama berikut lahir dari kebiasaan bahasa lain, sedangkan yang keempat lahir dari keluaran Clojure itu sendiri.

  • Menganggap immutable berarti program tidak memiliki state. Koleksi memang tidak berubah, tetapi bagian program yang memang harus berubah dari waktu ke waktu dikelola dengan tipe referensi seperti atom yang disediakan Clojure. Perbedaan yang perlu dipahami adalah perubahan membuat nilai baru, bukan menimpa nilai lama.
  • Mengharapkan koleksi berubah setelah diproses fungsi. Ekspresi (conj v 5) menghasilkan vector baru dan membuangnya bila hasilnya tidak ditampung, sedangkan v tetap sama. Ikat hasil operasi ke nama baru dengan def atau let, misalnya (def v-lengkap (conj v 5)).
  • Membangun hierarki kelas untuk data sederhana. Kebiasaan berorientasi objek mendefinisikan kelas untuk setiap entitas, sedangkan pada Clojure data lazim ditulis sebagai map biasa yang diolah fungsi. Kelas pada host hanya diperlukan untuk interop Java atau tipe yang memang diminta JVM.
  • Membaca 313/4 sebagai keanehan atau error. Nilai berbentuk pecahan adalah perilaku yang dimaksudkan, yaitu pembagian eksak bilangan bulat. Bungkus hasilnya dengan double apabila bentuk desimal yang diinginkan.

Ringkasan#

  • Clojure adalah dialek Lisp fungsional di atas JVM yang dikompilasi menjadi bytecode dan dapat memanggil kelas Java secara langsung.
  • Koleksi bersifat immutable, dan operasi transformasi menghasilkan koleksi baru dengan structural sharing, bukan mengubah koleksi lama.
  • Program disusun sebagai aliran data pada map, vector, dan set yang diolah fungsi generik seperti map, filter, dan reduce.
  • Tipe dinamis menempel pada nilai, sehingga pemeriksaan terjadi saat evaluasi dan umpan balik diperoleh seketika dari REPL.

Referensi#