S-expression dan Evaluasi

Membaca s-expression, memahami aturan evaluasi Clojure, serta quoting kode sebagai data.

Daftar isi

Topik ini membahas cara Clojure membaca dan mengevaluasi kode, mulai dari aturan pembacaan teks sumber menjadi form hingga tiga aturan evaluasi yang menentukan nilai setiap form. Topik ini juga membahas quoting, yaitu mekanisme yang menahan evaluasi sehingga kode dapat diperlakukan sebagai data biasa. Setelah topik ini selesai, keseluruhan bentuk program Clojure dapat dibaca dengan makna yang tepat, bukan sekadar diterjemahkan bentuknya.

Membaca form dan s-expression#

Clojure membaca teks sumber dengan menguraikannya menjadi unit yang disebut form. Unit paling dasarnya adalah s-expression (symbolic expression), yakni ekspresi simbolik berupa atom tunggal atau list berbatas tanda kurung yang menampung form lain. Atom mencakup angka, string, keyword, symbol, dan boolean, sedangkan list menampung beberapa form dalam satu tanda kurung. Bentuk (+ 1 2) adalah satu list berisi tiga atom, dan bentuk bersarang seperti (* 2 (+ 3 4)) adalah list yang berisi list lain.

Spasi, koma, dan baris baru berperan sebagai pemisah antaratom. Koma diperlakukan sama dengan spasi sehingga {:a 1, :b 2} dan {:a 1 :b 2} dibaca identik. Tanda titik koma memulai komentar yang berlaku sampai akhir baris. Tanda kurung wajib berpasangan karena kurung yang tidak tertutup menghasilkan error pembacaan sebelum kode dievaluasi satu baris pun. Konsekuensinya, indentasi hanyalah konvensi tampilan bagi manusia, sedangkan struktur program sepenuhnya ditentukan oleh pasangan tanda kurung.

Karena setiap list berstatus data, elemen pertama dan sisa elemennya dapat diperiksa seperti koleksi lain. Kemampuan tersebut menjadi dasar bagian quoting di bawah, dan karakter inilah yang membedakan Clojure dari bahasa yang teks sumbernya tidak dapat diperiksa sebagai data.

Tiga aturan evaluasi#

Evaluator menentukan nilai sebuah form menurut tiga aturan yang berlaku seragam. Literal, seperti angka, string, keyword, dan boolean, mengevaluasi ke nilai dirinya sendiri. Symbol dicari pada namespace saat ini untuk menemukan var yang dirujuknya, lalu nilai var tersebut dikembalikan. List dievaluasi sebagai pemanggilan, dengan elemen pertama menentukan fungsi yang dijalankan dan elemen sisanya menjadi argumen.

clojure Contoh: literal, symbol, dan var
; literal mengevaluasi ke nilai dirinya sendiri
(println 42)
(println "satu dua tiga")
(println :kunci)

; symbol diperiksa untuk menemukan var yang dirujuknya
(def nilai-ujian 88)
(println nilai-ujian)
(println (resolve 'nilai-ujian))
text Keluaran
42
satu dua tiga
:kunci
88
#'user/nilai-ujian

Contoh di atas memperlihatkan ketiga aturan sekaligus. Angka 42 dan keyword :kunci dikembalikan apa adanya, symbol nilai-ujian menghasilkan nilai varnya, dan list pemanggilan println menjalankan fungsinya. Skrip tanpa deklarasi ns berjalan pada namespace bernama user, sehingga resolve mengembalikan var #'user/nilai-ujian. Elemen pertama sebuah list tidak selalu fungsi biasa, karena dapat pula berupa bentuk khusus (special form) seperti def dan if, atau macro yang melakukan penguraian tambahan sebelum evaluasi. Bentuk khusus menentukan sendiri argumen mana yang perlu dievaluasi, misalnya if hanya mengevaluasi cabang yang dipilihnya.

Urutan evaluasi ekspresi bertingkat#

Pada pemanggilan fungsi biasa, evaluator memproses argumen satu per satu dari kiri ke kanan, lalu fungsi diterapkan pada seluruh hasilnya. Apabila salah satu argumen sendiri berupa list, argumen itu dievaluasi terlebih dahulu menurut aturan yang sama. Ekspresi terdalam dengan demikian selalu selesai lebih dahulu, dan proses ini berlaku berlapis tanpa kecuali.

clojure Contoh: urutan evaluasi bersarang
; ekspresi dalam dievaluasi lebih dahulu
(println (* 2 (+ 3 4)))
(println (/ 100 (+ 5 5)))
(println (+ 1 2 3 4))
text Keluaran
14
10
10

Struktur tanda kurung menunjukkan urutan komputasi secara langsung, berbeda dengan bahasa yang menentukan urutan lewat tabel prioritas operator. Bentuk (* 2 (+ 3 4)) dibaca dengan menyelesaikan (+ 3 4) dahulu, lalu hasilnya dikalikan dua. Kebiasaan membaca yang tepat adalah dari dalam ke luar, yaitu menuntaskan subekspresi terdalam sebelum membaca operasi luarnya. Kebiasaan ini cukup dipelajari satu kali karena berlaku seragam untuk seluruh bahasa, tanpa daftar pengecualian per operator.

Quoting: kode sebagai data#

Tanda kutip tunggal menahan evaluasi sebuah form sehingga form tersebut dikembalikan apa adanya sebagai data. Tanda ini merupakan singkatan dari bentuk khusus quote, sehingga '(+ 1 2) dan (quote (+ 1 2)) sama persis maknanya. Tanpa quote, list yang sama dievaluasi sebagai pemanggilan dan menghasilkan angka.

clojure Contoh: quote menahan evaluasi
; quote menahan evaluasi
(prn '(+ 1 2))
(prn (quote (+ 1 2)))

; tanpa quote, list dievaluasi sebagai pemanggilan
(println (+ 1 2))
text Keluaran
(+ 1 2)
(+ 1 2)
3

Hasil evaluasi '(+ 1 2) adalah list berisi tiga elemen, yaitu symbol +, angka 1, dan angka 2. Karena hasilnya data biasa, fungsi koleksi seperti count, first, second, dan nth bekerja padanya, dan fungsi eval dapat mengevaluasinya kembali menjadi nilai.

clojure Contoh: mengoperasikan kode sebagai data
(def form '(+ 2 (* 3 4)))
(println (count form))
(println (first form))
(println (second form))
(prn (nth form 2))
(println (eval form))
text Keluaran
3
+
2
(* 3 4)
14

Pola ini dikenal sebagai kode sebagai data (code as data), yaitu program dan struktur datanya menggunakan representasi yang sama. Fungsi eval pada praktik sehari-hari jarang dipanggil langsung, tetapi kemampuan membaca struktur form inilah yang nantinya menjadi bekal memahami macro. Untuk tahap ini, yang perlu dikuasai adalah kepastian bahwa kode yang dikutip adalah data, sedangkan kode tanpa quote adalah instruksi yang dievaluasi.

Kesalahan umum#

Menyisakan quote pada form yang seharusnya dievaluasi. Bentuk '(+ 1 2) yang dipanggil menghasilkan list data, bukan angka 3, sehingga perhitungan berikutnya berhenti dengan pesan tentang tipe yang tidak sesuai. Penyebabnya adalah quote yang tertinggal dari penyalinan contoh. Perbaikannya dengan menghapus quote apabila tujuannya menjalankan perhitungan, dan mempertahankannya hanya apabila form memang digunakan sebagai data.

Menambah tanda kurung berlebih untuk mengelompokkan operasi. Bentuk ((+ 1 2)) dibaca sebagai pemanggilan dengan elemen pertamanya hasil 3, sehingga evaluator mencoba memanggil angka sebagai fungsi dan melempar error Long cannot be cast to clojure.lang.IFn. Pesan ini selalu berarti sebuah nilai digunakan pada posisi fungsi. Perbaikannya dengan menghapus kurung dalam, karena pengelompokan di Clojure dilakukan lewat bersarangnya list, bukan lewat lapisan kurung tambahan.

Menulis symbol yang tidak dikenal sehingga muncul pesan Unable to resolve symbol. Pesan ini muncul apabila nama yang digunakan tidak terikat pada namespace saat ini, biasanya akibat salah ketik atau def yang belum dievaluasi. Perbaikannya dengan memeriksa ejaan nama dan memastikan var yang dirujuk sudah didefinisikan terlebih dahulu.

Ringkasan#

  • Reader mengubah teks sumber menjadi form, berupa atom atau list dengan tanda kurung yang wajib berpasangan.
  • Evaluasi mengikuti tiga aturan, yaitu literal bernilai dirinya sendiri, symbol merujuk var, dan list menjadi pemanggilan.
  • Argumen dievaluasi dari kiri ke kanan, dan subekspresi terdalam selalu selesai lebih dahulu.
  • Quote menahan evaluasi sehingga form menjadi data yang dapat dihitung, diambil elemennya, dan dievaluasi ulang dengan eval.

Referensi#