logo
hero image

Kotlin DSL

Membuat custom Domain Specific Language (DSL) pada Kotlin.
28 November 2023 · 7 Minutes
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.
Martin Fowler

Salah satu alasan mengapa bahasa Kotlin menjadi populer dan banyak disukai oleh para developer adalah karena syntax-nya yang ringkas dan mudah dimengerti.

Untuk menulis kode yang lebih ringkas, Kotlin memungkinkan kita untuk membuat Domain Specific Language atau DSL!

Apa itu DSL ?

DSL (Domain Specific Language) merupakan bahasa / syntax yang didesain khusus untuk bagian spesifik dari program kita. Kebalikan dari DSL adalah GPL (General Purpose Language) yaitu bahasa yang bisa digunakan untuk banyak bagian dari program kita, Contoh dari GPL adalah bahasa pemrograman Kotlin, Swift, dll.

Karena DSL didesain khusus untuk bagian tertentu dari program kita serta scope-nya lebih terfokus, maka hasil dari DSL yang kita buat akan jadi lebih ringkas & mudah untuk dibaca. Sehingga orang lain yang tidak mengerti detail implementasi dari kode kita pun tetap bisa memahami makna dari kode yang kita tulis.

Kotlin DSLKotlin DSL

Contoh DSL

Berikut adalah contoh dari Domain Specific Language DSL pada Kotlin:

Gradle DSL

Salah satu contoh DSL yang paling populer adalah konfigurasi Gradle.

icon
1plugins {
2 id("org.jetbrains.kotlin.jvm") version "1.8.20"
3}
4
5repositories {
6 mavenCentral()
7}
8
9dependencies {
10 implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
11 testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
12}

Koin DSL

Atau jika kamu pernah menggunakan library Koin untuk melakukan Dependency Injection, Koin juga mempunyai DSL sendiri.

icon
1val appModule = module {
2 single { Service() }
3}
4
5startKoin {
6 logger()
7 modules(appModule)
8}

Karakteristik DSL

DSL mempunyai 2 karakteristik utama yaitu Context dan Fluency.

Context

Dengan adanya Context, kita tidak perlu lagi menuliskan kode-kode yang repetitif.

Contohnya jika tanpa Context, mungkin kode yang kita tulis akan seperti ini:

Tanpa Context
icon
1val contact = Contact()
2contact.email = "me@alfin.dev"
3contact.phone = "088233104788"
4contact.ig = "alfinsyahruddin"
5myProfile.contact = contact

Namun dengan Context, kode kita akan menjadi lebih ringkas:

Dengan Context
icon
1profile {
2 contact {
3 email = "me@alfin.dev"
4 phone = "088233104788"
5 ig = "alfinsyahruddin"
6 }
7}

Fluency

Dengan DSL, kita bisa menulis kode layaknya bahasa manusia sehingga kode kita akan lebih mudah untuk dibaca.

Perhatikan 2 kode di bawah ini, manakah menurutmu yang lebih mudah untuk dimengerti ketika pertama kali membaca kode tersebut? 😁

Regular Kotlin Code
icon
1val dob = LocalDate.of(2003, Month.FEBRUARY, 10)
DSL
icon
1val dob = 10 feb 2003

Internal vs External DSL

Ketika kita membuat DSL pada General Purpose Language seperti Kotlin, sebenarnya kita sedang membuat Internal DSL, karena kita menggunakan bahasa yang sudah ada (Kotlin). Kelebihannya kita bisa memanfaatkan fitur2 dari bahasa Kotlin tersebut seperti for-loop, Scope Functions, dll.

Di samping itu, ada juga External DSL, yaitu ketika kita membuat DSL dengan grammar, lexer, dan parser yang benar-benar baru. Contoh dari External DSL yang paling populer adalah SQL.

Membuat Custom DSL

Sekarang, kita akan belajar bagaimana cara membuat custom DSL pada Kotlin! 😃

Kita akan mengubah kode berikut:

Regular Kotlin Code
icon
1val myProfile = Profile()
2myProfile.name = "Alfin"
3myProfile.dob = LocalDate.of(2003, Month.FEBRUARY, 10)
4
5val contact = Contact()
6contact.email = "me@alfin.dev"
7myProfile.contact = contact
8
9myProfile.interests += "TECH - Mobile Engineering"
10myProfile.interests += "TECH - Augmented Reality"
11myProfile.interests += "PHILOSOPHY - Libertarianism"

Menjadi seperti ini, tanpa mengubah struktur datanya:

DSL
icon
1val myProfile = profile {
2 name = "Alfin"
3 dob = 10 feb 2003
4
5 contact {
6 email = "me@alfin.dev"
7 }
8
9 interests {
10 tech("Mobile Engineering")
11 tech("Augmented Reality")
12 philosophy("Libertarianism")
13 }
14}

Data Structure

Berikut adalah struktur data dari class Profile dan Contact :

icon
1class Profile {
2 var name: String? = null
3 var dob: LocalDate? = null
4 var contact: Contact? = null
5 var interests = mutableListOf<String>()
6}
7
8class Contact {
9 var email: String? = null
10}

1. Lambda dengan Context Object

Agar kita bisa menggunakan syntax seperti ini:

icon
1val myProfile = profile {
2 name = "Alfin"
3
4 contact {
5 email = "me@alfin.dev"
6 }
7}

Silakan tambahkan code berikut:

icon
1fun profile(builder: Profile.() -> Unit): Profile {
2 return Profile().apply(builder)
3}
4
5fun Profile.contact(builder: Contact.() -> Unit) {
6 contact = Contact().apply(builder)
7}

Profile.() -> Unit akan memberikan Context Object dengan tipe data Profile di dalam lambda kita.

icon
1val myProfile = profile { // this: Profile

apply merupakan salah satu dari 5 Scope Functions pada Kotlin. Baca juga: Scope Functions

2. Infix Function

Agar kita bisa menggunakan syntax seperti ini:

icon
1dob = 10 feb 2003

Silakan tambahkan code berikut:

icon
1infix fun Int.feb(year: Int): LocalDate = LocalDate.of(year, Month.FEBRUARY, this)

Infix Function memungkinkan kita untuk memanggil sebuah function tanpa perlu menuliskan tanda titik, kurang buka & kurung tutup.

10 feb 2003 jika tanpa Infix Function, kita perlu menuliskannya seperti ini: 10.feb(2003)

3. Lambda dengan Scope

Daripada kita menulis kode yang repetitif dan meng-hardcode topic seperti TECH, PHILOSOPHY seperti ini:

icon
1myProfile.interests += "TECH - Mobile Engineering"
2myProfile.interests += "TECH - Augmented Reality"
3myProfile.interests += "PHILOSOPHY - Libertarianism"

Kita bisa merefactornya menggunakan Lambda dengan custom Scope menjadi seperti ini:

icon
1interests { // this: InterestScope
2 tech("Mobile Engineering")
3 tech("Augmented Reality")
4 philosophy("Libertarianism")
5}

Silakan tambahkan kode berikut:

icon
1fun Profile.interests(builder: InterestScope.() -> Unit) {
2 interests += InterestScope().apply(builder).interests
3}
4
5class InterestScope {
6 var interests: List<String> = mutableListOf()
7 private set
8
9 fun tech(topic: String) {
10 interests += "TECH - $topic"
11 }
12
13 fun philosophy(topic: String) {
14 interests += "PHILOSOPHY - $topic"
15 }
16}

4. Context Receiver

InterestScope yang baru saja kita buat sebenarnya masih ada issue, yaitu kita bisa menggunakan Scope tersebut di mana pun, bukankah seharusnya kita hanya bisa menggunakan Scope tersebut di dalam lambda profile saja ?

Untuk mengatasi hal tersebut, kita bisa memanfaatkan fitur pada Kotlin bernama Context Receiver, silakan modifikasi kodenya menjadi seperti ini:

icon
2private object ProfileContext
3
4fun Profile.interests(builder: InterestScope.() -> Unit) {
6 with(ProfileContext) {
7 interests += InterestScope().apply(builder).interests
9 }
10}
11
13context(ProfileContext)
14class InterestScope {
15 var interests: List<String> = mutableListOf()
16 private set
17
18 fun tech(topic: String) {
19 interests += "TECH - $topic"
20 }
21
22 fun philosophy(topic: String) {
23 interests += "PHILOSOPHY - $topic"
24 }
25}

Jadi, ketika kita mencoba mengakses InterestScope dari luar lambda profile, maka akan muncul error seperti ini:

Context ReceiverContext Receiver

NOTE: Pada saat artikel ini ditulis, Context Receiver merupakan fitur yang masih experimental, untuk menggunakannya, silakan tambahkan kode berikut di dalam file build.gradle.kts:

build.gradle.kts
icon
1tasks.withType(KotlinCompile::class.java) {
2 kotlinOptions.freeCompilerArgs += "-Xcontext-receivers"
3}

Berikut adalah kode lengkap dari tutorial ini:

App.kt
icon
1package dev.alfin.kotlincode
2
3import java.time.LocalDate
4import java.time.Month
5
6fun main() {
7 val myProfile = profile {
8 name = "Alfin"
9 dob = 10 feb 2003
10
11 contact {
12 email = "me@alfin.dev"
13 }
14
15 interests {
16 tech("Mobile Engineering")
17 tech("Augmented Reality")
18 philosophy("Libertarianism")
19 }
20 }
21}
22
23
24// MARK: - Data Structure
25
26class Profile {
27 var name: String? = null
28 var dob: LocalDate? = null
29 var contact: Contact? = null
30 var interests = mutableListOf<String>()
31}
32
33class Contact {
34 var email: String? = null
35}
36
37
38// MARK: - DSL
39
40fun profile(builder: Profile.() -> Unit): Profile {
41 return Profile().apply(builder)
42}
43
44fun Profile.contact(builder: Contact.() -> Unit) {
45 contact = Contact().apply(builder)
46}
47
48infix fun Int.feb(year: Int): LocalDate = LocalDate.of(year, Month.FEBRUARY, this)
49
50private object ProfileContext
51
52fun Profile.interests(builder: InterestScope.() -> Unit) {
53 with(ProfileContext) {
54 interests += InterestScope().apply(builder).interests
55 }
56}
57
58context(ProfileContext)
59class InterestScope {
60 var interests: List<String> = mutableListOf()
61 private set
62
63 fun tech(topic: String) {
64 interests += "TECH - $topic"
65 }
66
67 fun philosophy(topic: String) {
68 interests += "PHILOSOPHY - $topic"
69 }
70}

Oke mungkin itu saja yang bisa saya bagikan kali ini, kalau kamu merasa artikel ini bermanfaat silakan Like & Share artikel ini ke teman-teman kamu atau jika kamu punya pertanyaan, tulis aja di kolom komentar, Thank you! 😁 🙏

Referensi

Android Development
Kotlin

Written by :
Alfin Syahruddin
Developer · Stock Trader · Libertarian · Freethinker

Always open to new ideas. 🕊️

Loading...

Related articles

Articles that you might want to read.

hero image
Scope Functions

let, with, run, apply, dan also pada Kotlin.

12 November 2023 · 7 Minutes
Android Development
Kotlin
hero image
Testable Code

Menulis kode yang mudah untuk di-test pada Swift.

1 May 2023 · 8 Minutes
iOS Development
Swift
Testing
hero image
Gimana cara menambahkan fitur Multi Bahasa pada aplikasi React-mu?

Tutorial multilingual menggunakan react-i18next

21 August 2020 · 4 Minutes
React
Internationalization
Web Development
All rights reserved © Alfin Syahruddin · 2019
RSS Feed