spec.alpha adalah pustaka bawaan Clojure untuk mendeskripsikan bentuk data dan fungsi. Topik ini membahas pendefinisian spec, yaitu deskripsi bentuk data yang dapat diperiksa, atas keyword melalui s/def. Pemeriksaan dengan s/valid?, penyepakatan data dengan s/conform, diagnosis pelanggaran dengan s/explain, deskripsi map dengan s/keys, deskripsi fungsi dengan s/fdef, serta pembuatan contoh data dengan s/exercise dibahas pada bagian-bagian berikutnya. Contoh sepanjang topik menggunakan data nilai ujian siswa, dan seluruh keluaran berasal dari eksekusi nyata pada Clojure 1.12. Setelah topik ini selesai, bentuk data yang beredar di dalam program dapat dideskripsikan, diperiksa, dan didiagnosis apabila pelanggarannya terjadi.
Mendefinisikan spec atas keyword#
Spec didefinisikan dengan fungsi s/def dan didaftarkan atas sebuah keyword. Pendaftarannya bersifat global, sehingga dua spec atas keyword yang sama saling menimpa. Keyword bernaung seperti :siswa/nilai menjadi pilihan yang lebih aman daripada :nilai karena kemungkinan bertabrakan dengan keyword modul lain lebih kecil. Predikat apa pun dapat berperan sebagai spec, mulai dari fungsi satu argumen seperti string? sampai spec gabungan seperti s/int-in yang menerima rentang bilangan bulat. Rentang s/int-in mencakup batas bawah tetapi tidak mencakup batas atas, sehingga (s/int-in 0 100) menampung bilangan 0 sampai 99.
(require '[clojure.spec.alpha :as s])
(s/def :siswa/nama string?)
(s/def :siswa/nilai (s/int-in 0 100))
(println "nilai 82 :" (s/valid? :siswa/nilai 82))
(println "nilai 105 :" (s/valid? :siswa/nilai 105))
(println "nama Citra :" (s/valid? :siswa/nama "Citra"))
(println "nama 17 :" (s/valid? :siswa/nama 17))nilai 82 : true
nilai 105 : false
nama Citra : true
nama 17 : falseFungsi s/valid? mengembalikan true atau false tanpa memberi tahu penyebabnya. Nilai 105 dinyatakan tidak valid karena berada di luar rentang, demikian pula 17 yang bukan string. Pemeriksaan boolean seperti ini murah dan cocok untuk gerbang data yang hanya perlu lolos atau ditolak. Keyword yang sama digunakan kembali sebagai nama spec di seluruh program, sehingga fungsi pemeriksa, pesan error, dan data yang dikirim mengikuti kosakata yang sama.
conform dan penanda tidak valid#
s/conform mengembalikan nilai itu sendiri apabila data sah. Apabila data tidak sah, keluarannya adalah keyword khusus :clojure.spec.alpha/invalid, penanda bawaan spec.alpha untuk kegagalan penyepakatan. Fungsi s/invalid? memeriksa penanda tersebut, dan pemanggilannya wajib dilakukan setiap kali hasil conform digunakan lebih lanjut. Pola ini melengkapi s/valid?: apabila program hanya perlu menolak atau menerima, s/valid? sudah cukup, sedangkan apabila data sah ingin dilanjutkan ke tahap berikutnya, s/conform mengembalikan datanya dalam satu langkah.
(require '[clojure.spec.alpha :as s])
(s/def :siswa/nilai (s/int-in 0 100))
(println "conform 82 :" (s/conform :siswa/nilai 82))
(println "conform 105 :" (s/conform :siswa/nilai 105))
(println "invalid? :" (s/invalid? (s/conform :siswa/nilai 105)))conform 82 : 82
conform 105 : :clojure.spec.alpha/invalid
invalid? : truePada spec sederhana seperti ini, conform mengembalikan data tanpa perubahan. Spec yang lebih kompleks menggunakan conform untuk mengubah data menjadi bentuk terstruktur, misalnya s/keys dengan kunci tanpa naungan yang menyusun ulang nama kunci. Karena keluaran conform bergantung pada spec-nya, memeriksa s/invalid? sebelum menggunakan hasilnya tetap menjadi aturan yang berlaku untuk semua bentuk spec.
explain untuk diagnosis#
s/explain mencetak laporan pelanggaran ke keluaran standar, sedangkan s/explain-data mengembalikan laporan yang sama sebagai data. Laporan data memuat kunci :clojure.spec.alpha/problems berupa kumpulan pelanggaran. Setiap pelanggaran membawa :path untuk lokasi dalam spec, :pred untuk predikat yang gagal, :val untuk nilai yang ditolak, :via untuk rangkaian spec yang dilalui, serta :in untuk jalur menuju nilai di dalam data. Bentuk data ini dapat diproses program, misalnya dikirim sebagai isi pesan error, sedangkan bentuk cetaknya cocok untuk sesi pengembangan.
(require '[clojure.spec.alpha :as s])
(s/def :siswa/nilai (s/int-in 0 100))
(s/explain :siswa/nilai 105)
(prn (:clojure.spec.alpha/problems (s/explain-data :siswa/nilai 105)))105 - failed: (int-in-range? 0 100 %) spec: :siswa/nilai
[{:path [], :pred (clojure.core/fn [%] (clojure.spec.alpha/int-in-range? 0 100 %)), :val 105, :via [:siswa/nilai], :in []}]Baris pertama keluaran berasal dari s/explain, yang menyebut nilai 105 gagal terhadap predikat rentang. Baris kedua adalah vektor pelanggaran dari s/explain-data yang dicetak dengan prn. Nilai :pred menampilkan predikat dalam bentuk lengkapnya, clojure.spec.alpha/int-in-range?, sehingga penyebab kegagalan dapat dibaca tanpa menebak. Field :path dan :in yang masih kosong pada contoh ini akan terisi apabila pelanggaran terjadi di bagian dalam data, sebagaimana diperlihatkan pada spec map di bawah.
Deskripsi map dengan s/keys#
Map dideskripsikan dengan s/keys. Opsi :req menerima daftar keyword yang wajib ada, dan setiap keyword dirujuk ke spec yang sudah didaftarkan atas namanya, sehingga deskripsi map tersusun atas spec skalar yang sama dengan yang digunakan satu per satu. Kunci yang tidak dinyatakan tidak ditolak, karena s/keys hanya memeriksa kunci yang disebut dan membiarkan kunci lain lolos. Deskripsi map nilai ujian berikut menggunakan dua spec yang sudah didefinisikan sebelumnya.
(require '[clojure.spec.alpha :as s])
(s/def :siswa/nama string?)
(s/def :siswa/nilai (s/int-in 0 100))
(s/def :siswa/uas (s/keys :req [:siswa/nama :siswa/nilai]))
(def ujian {:siswa/nama "Ali" :siswa/nilai 82})
(println "valid? :" (s/valid? :siswa/uas ujian))
(print "conform: ")
(prn (s/conform :siswa/uas ujian))valid? : true
conform: #:siswa{:nama "Ali", :nilai 82}Keluaran conform memperlihatkan #:siswa{:nama "Ali", :nilai 82}, notasi map bernaung yang setara dengan map berisi :siswa/nama dan :siswa/nilai. Spec map dengan kunci bernaung tidak mengubah data apa pun pada saat conform, sehingga keluarannya identik dengan masukannya. Dua pelanggaran yang paling sering terjadi pada data map diperiksa berikut, yaitu nilai di luar rentang dan kunci wajib yang tidak ada.
(require '[clojure.spec.alpha :as s])
(s/def :siswa/nama string?)
(s/def :siswa/nilai (s/int-in 0 100))
(s/def :siswa/uas (s/keys :req [:siswa/nama :siswa/nilai]))
(println "== nilai di luar rentang ==")
(s/explain :siswa/uas {:siswa/nama "Budi" :siswa/nilai 105})
(println "== kunci wajib tidak ada ==")
(s/explain :siswa/uas {:siswa/nilai 74})== nilai di luar rentang ==
105 - failed: (int-in-range? 0 100 %) in: [:siswa/nilai] at: [:siswa/nilai] spec: :siswa/nilai
== kunci wajib tidak ada ==
#:siswa{:nilai 74} - failed: (contains? % :siswa/nama) spec: :siswa/uasPada pelanggaran pertama, bagian in: [:siswa/nilai] dari laporan teks menunjuk nilai di dalam map, sehingga lokasi kesalahannya tepat pada kunci :siswa/nilai, bukan pada map keseluruhan. Pada pelanggaran kedua, predikat contains? yang gagal berasal dari pemeriksaan kunci wajib :siswa/nama, dan laporannya menyebut spec :siswa/uas karena kunci memang tidak ditemukan. Diagnosis semacam ini membuat perbaikan data tidak perlu dilakukan dengan pencetakan map satu per satu.
Deskripsi fungsi dengan s/fdef#
Fungsi juga dapat dideskripsikan. s/fdef mendaftarkan spec untuk sebuah fungsi pada tiga bagian, yaitu :args untuk argumen, :ret untuk keluaran, dan :fn untuk hubungan antara keduanya. Bagian yang berdampak langsung adalah :args, karena namespace clojure.spec.test.alpha menyediakan stest/instrument yang membungkus fungsi dengan pemeriksaan argumen. Panggilan yang melanggar spec argumen melempar ExceptionInfo, dan fungsi aslinya tidak dijalankan. Bagian :ret dan :fn digunakan ketika fungsi diperiksa secara generatif, mekanisme yang dibahas pada topik Testing Generatif.
(require '[clojure.spec.alpha :as s]
'[clojure.spec.test.alpha :as stest])
(defn status-lulus
[nilai]
(if (>= nilai 75) "lulus" "belum lulus"))
(s/def :penilaian/nilai (s/int-in 0 100))
(s/fdef status-lulus
:args (s/cat :nilai :penilaian/nilai)
:ret string?)
(stest/instrument)
(println "status 82 :" (status-lulus 82))
(try
(status-lulus 150)
(catch Exception e
(println "status 150:" (class e))))status 82 : lulus
status 150: clojure.lang.ExceptionInfoPanggilan (status-lulus 82) lolos karena 82 berada dalam rentang, sedangkan (status-lulus 150) ditolak oleh instrument dan yang tercetak adalah kelas exception-nya, clojure.lang.ExceptionInfo. Tanpa instrument, panggilan kedua tetap berjalan dan menghasilkan “lulus” untuk nilai yang seharusnya tidak pernah diterima fungsi. Deskripsi argumen melalui :args menggunakan s/cat, yaitu spec regex yang mengikat setiap posisi argumen ke sebuah nama.
Menghasilkan contoh dengan s/exercise#
Spec yang sudah terdaftar dapat menghasilkan contoh data melalui s/exercise. Fungsi ini mengembalikan rangkaian pasangan berupa contoh dan bentuk conform-nya, sehingga perilaku spec dapat diamati tanpa menulis data contoh secara manual. Contoh dihasilkan oleh generator, pembangkit data acak dari pustaka test.check, sehingga keluarannya berbeda pada setiap sesi REPL. Karena pustaka test.check belum berada di classpath topik ini, pemanggilannya diperlihatkan tanpa eksekusi, sedangkan contoh yang benar-benar dijalankan beserta keluarannya ada pada topik Testing Generatif.
; tidak-dijalankan
; memerlukan pustaka test.check di classpath, dieksekusi pada topik Testing Generatif
(require '[clojure.spec.alpha :as s])
(s/def :siswa/nilai (s/int-in 0 100))
; tiga pasangan berupa contoh dan bentuk conform-nya
(s/exercise :siswa/nilai 3)Kesalahan umum#
- Memperlakukan hasil
s/conformatas data tidak valid sebagai data sah. Penanda:clojure.spec.alpha/invalidadalah keyword yang dapat tersimpan dan terkirim seperti nilai biasa. Perbaikannya dengan memeriksas/invalid?sebelum hasil conform digunakan. - Mendefinisikan spec atas keyword tanpa naungan, misalnya
:nilaialih-alih:siswa/nilai. Karena pendaftarans/defbersifat global, spec lain atas keyword yang sama menimpa yang lama tanpa peringatan. Perbaikannya dengan memberi naungan sesuai ranah data. - Mengira
s/keysmenolak kunci tambahan. Opsi:reqhanya mewajibkan kunci yang disebut, sedangkan kunci lain lolos begitu saja. Apabila kunci ekstra memang harus ditolak, periksa jumlah kunci secara terpisah atau gunakan pustaka schema seperti malli yang dibahas pada topik Pengenalan Malli.
Ringkasan#
s/defmendaftarkan spec atas keyword secara global, dan predikat apa pun beserta spec gabungan sepertis/int-indapat berperan sebagai spec.s/valid?menjawab true atau false, sedangkans/conformmengembalikan nilai atau penanda:clojure.spec.alpha/invalidyang wajib diperiksa dengans/invalid?.s/explaindans/explain-datamenyediakan diagnosis pelanggaran, baik sebagai teks maupun sebagai data berisi:pred,:val,:via, dan:in.s/keysmenyusun spec map dari spec kuncinya,s/fdefdenganstest/instrumentmemeriksa argumen fungsi, dans/exercisemenghasilkan contoh data dari generator.
Referensi#
- Specification Guide — Clojure — Panduan resmi spec.alpha: pendefinisian spec, conform, explain, fdef, dan generator.