Sebuah DSL (domain-specific language) adalah gaya penulisan kode yang bentuknya menyerupai bahasa untuk satu bidang masalah tertentu. Kotlin menghasilkan DSL yang type-safe melalui pola type-safe builder, yaitu fungsi yang menerima lambda dengan receiver sehingga struktur bersarang dapat ditulis seperti kalimat, tetapi tetap diperiksa oleh compiler. Topik ini membangun sebuah pembangun dokumen HTML mini yang berjalan penuh, menjelaskan mekanisme lambda dengan receiver di baliknya, lalu menunjukkan cara @DslMarker menolak pemanggilan yang keliru di dalam struktur bersarang.
Lambda dengan receiver#
Fondasi pola builder adalah tipe lambda dengan receiver. Tipe lambda biasa seperti (String) -> Unit menerima nilainya sebagai parameter, sedangkan tipe DokumenHtml.() -> Unit menempelkan objek DokumenHtml pada lambda sebagai receiver, sehingga di dalam badan lambda objek itu tersedia sebagai this. Panggilan milik receiver pun dapat ditulis tanpa awalan, dan perbedaannya terlihat pada dua deklarasi berikut.
// tidak-dijalankan
val biasa: (StringBuilder) -> Unit = { teks -> teks.append("dengan parameter") }
val denganReceiver: StringBuilder.() -> Unit = { append("dengan receiver") }Pada biasa, lambda menamai parameternya sendiri, sedangkan pada denganReceiver, fungsi append dipanggil langsung karena tersedia dari this. Sifat this di dalam lambda dengan receiver sama dengan this di dalam fungsi extension, sehingga kebiasaan yang berlaku pada extension berlaku juga di sini. Perbedaannya terletak pada pemakaiannya, yaitu lambda dengan receiver dibuat dan dijalankan oleh fungsi yang menjadi pintu masuk builder, sedangkan fungsi extension menyediakan perilaku tambahan bagi tipe yang sudah ada. Pola builder memanfaatkan bentuk kedua: sebuah fungsi factory membuat objek builder, menjalankan lambda argumennya pada objek itu, lalu mengembalikan hasil jadinya. Pemanggil yang membaca kode builder tidak pernah melihat objek antaranya, yang tampak hanyalah struktur dokumen yang sedang disusun.
Pembangun dokumen HTML mini#
Builder berikut menyusun dokumen HTML sederhana dengan dua tingkat struktur, yaitu dokumen dan daftar di dalamnya. Fungsi p menambahkan paragraf, fungsi daftar membuka lingkup baru untuk daftar, dan fungsi butir mengisi lingkup tersebut. Seluruh keluaran dikumpulkan oleh StringBuilder di dalam builder, sedangkan fungsi factory dokumenHtml mengembalikan string jadinya.
@DslMarker
annotation class PembatasDokumen
@PembatasDokumen
class DokumenHtml {
val isi = StringBuilder()
fun p(teks: String) {
isi.append(" <p>$teks</p>\n")
}
fun daftar(bangun: DaftarHtml.() -> Unit) {
isi.append(" <ul>\n")
DaftarHtml(isi).bangun()
isi.append(" </ul>\n")
}
}
@PembatasDokumen
class DaftarHtml(private val hasil: StringBuilder) {
fun butir(teks: String) {
hasil.append(" <li>$teks</li>\n")
}
}
fun dokumenHtml(bangun: DokumenHtml.() -> Unit): String {
val dokumen = DokumenHtml()
dokumen.bangun()
return "<html>\n${dokumen.isi}</html>"
}
fun main() {
val html = dokumenHtml {
p("Jadwal piket kelas")
daftar {
butir("Senin: Ali")
butir("Selasa: Budi")
butir("Rabu: Citra")
}
p("Piket dimulai pukul 06.30")
}
println(html)
}# compiler Kotlin dan pustaka standar yang dipakai jalur ini
export PATH=/home/kal/my-project/tools/kotlinc/bin:/home/kal/my-project/tools/jdk-merged/bin:$PATH
export STDLIB=/home/kal/my-project/tools/kotlinc/lib/kotlin-stdlib.jar
# kompilasi main.kt menjadi out.jar, lalu jalankan programnya
kotlinc main.kt -d out.jar && java -cp "out.jar:$STDLIB" MainKt<html>
<p>Jadwal piket kelas</p>
<ul>
<li>Senin: Ali</li>
<li>Selasa: Budi</li>
<li>Rabu: Citra</li>
</ul>
<p>Piket dimulai pukul 06.30</p>
</html>Cara kerjanya dapat ditelusuri dari tanda kurung kurawal. Pemanggilan dokumenHtml { ... } menjalankan lambda argumennya pada objek DokumenHtml, sehingga p dan daftar dipanggil pada receiver itu tanpa awalan. Pemanggilan daftar { ... } membuat objek DaftarHtml yang berbagi StringBuilder yang sama, kemudian menjalankan lambda miliknya, sehingga butir hanya tersedia di dalam lingkup daftar. Indentasi pada keluaran mengikuti kedalaman sarangnya, dan struktur kode di bagian main membaca seperti dokumen yang dihasilkannya.
Manfaat pola ini paling terasa dari sisi pemanggilnya. Tanpa builder, penyusunan dokumen yang sama berarti memanggil append berulang kali sambil mengatur indentasi dan tag penutup secara manual, dan kesalahan urutan tidak tertangkap oleh siapa pun. Dengan builder, setiap pemanggilan berupa fungsi bernama yang parameternya diperiksa tipenya, sehingga pemanggilan yang salah tempat dapat ditolak compiler. Biaya pembuatannya hanya dua kelas kecil dan satu fungsi factory, sedangkan manfaatnya dibayar kembali setiap kali dokumen disusun ulang.
Larangan bersarang dengan DslMarker#
Tanpa pengaman tambahan, blok daftar { ... } masih dapat memanggil p milik dokumen luar, karena receiver luar tetap terlihat dari dalam lambda yang bersarang. Pemanggilan semacam itu lolos compiler tanpa pesan error, tetapi paragrafnya ikut tercetak di dalam daftar sehingga struktur HTML-nya kacau. Kelas DokumenHtml dan DaftarHtml pada program di atas sudah dilindungi, karena anotasi PembatasDokumen diberi tanda @DslMarker, yang menyatakan bahwa kedua kelas bertanda itu tidak boleh dijadikan receiver yang bertumpuk. Compiler pun menolak pemanggilan berikut.
// tidak-dijalankan
fun contohPelanggaran() {
dokumenHtml {
daftar {
p("paragraf tidak boleh berada di dalam daftar")
}
}
}Pada potongan tersebut, compiler melaporkan bahwa p tidak dapat dipanggil dengan receiver implisit di konteks itu, dengan pesan 'fun p(teks: String): Unit' cannot be called in this context with an implicit receiver. Kesalahan tertangkap saat kompilasi, bukan saat dokumen jadinya dibaca, dan itulah arti kata type-safe pada pola ini.
Ringkasan#
- Pola type-safe builder memakai fungsi berparameter lambda dengan receiver, sehingga struktur bersarang ditulis seperti kalimat tetapi tetap diperiksa compiler.
- Di dalam lambda dengan receiver, objeknya tersedia sebagai
this, sehingga fungsi miliknya dipanggil tanpa awalan. @DslMarkermelarang pemanggilan milik receiver luar dari dalam receiver yang lebih dalam, sehingga penempatan yang keliru ditolak saat kompilasi.- Builder bekerja pada kolektor internal seperti
StringBuilderdan mengembalikan hasil jadinya melalui fungsi factory-nya.
Referensi#
- Type-safe builders — Kotlin Documentation
— Dokumentasi resmi pola type-safe builder, termasuk peran
@DslMarkerdalam membatasi sarang receiver. - Higher-order functions and lambdas — Kotlin Documentation — Aturan fungsi dengan receiver pada tipe lambda yang menjadi dasar pola builder.