Kotlin adalah bahasa multi-paradigm — kita dapat menulis kode imperatif, object-oriented, atau fungsional. Pada chapter ini kita akan melihat pola desain fungsional yang paling umum dipakai di Kotlin: pipeline map/filter/reduce, function composition, immutability, dan operasi side-effect-free. Kita juga akan membandingkan gaya fungsional Kotlin dengan Haskell murni untuk memahami apa yang bisa dan tidak bisa dilakukan di Kotlin. Pemahaman pola ini membantu kita menulis kode yang lebih modular, mudah diuji, dan kebal terhadap bug akibat mutasi state tak terduga.

Prinsip Utama Functional Design #

Empat prinsip utama desain fungsional yang relevan di Kotlin:

  1. Immutability — pakai val dan data class copy() alih-alih mutasi.
  2. Pure function — function tidak bergantung pada atau mengubah state luar.
  3. Function composition — gabung function kecil menjadi pipeline.
  4. Side-effect-free — operasi tidak mengubah input, hanya mengembalikan output baru.

Kotlin tidak mengharuskan prinsip ini secara ketat seperti Haskell, tetapi memilih pola fungsional membantu menulis kode yang lebih jelas dan lebih mudah diuji.

Pola Map/Filter/Reduce #

Pola paling dasar dalam functional design adalah pipeline transformasi koleksi: map mengubah setiap elemen, filter menyaring berdasarkan predikat, dan fold/reduce menggabungkan elemen menjadi satu nilai.

kotlin
// functional_design_patterns.kt
// Functional design: map/filter/reduce, composition, immutability.

data class Transaksi(val kategori: String, val jumlah: Int, val tipe: String)

fun main() {
    val transaksi = listOf(
        Transaksi("Makanan", 50_000, "PENGELUARAN"),
        Transaksi("Gaji", 5_000_000, "PENDAPATAN"),
        Transaksi("Transport", 25_000, "PENGELUARAN"),
        Transaksi("Bonus", 1_000_000, "PENDAPATAN"),
        Transaksi("Hiburan", 75_000, "PENGELUARAN"),
        Transaksi("Makanan", 30_000, "PENGELUARAN")
    )

    // 1. Pipeline map/filter/reduce — pendekatan fungsional.
    val totalPengeluaranMakanan = transaksi
        .filter { it.tipe == "PENGELUARAN" && it.kategori == "Makanan" }
        .map { it.jumlah }
        .fold(0) { acc, n -> acc + n }
    println("Total pengeluaran makanan: $totalPengeluaranMakanan")

    // 2. Group-by + agregasi (pure functional style).
    val ringkasanPerKategori = transaksi
        .filter { it.tipe == "PENGELUARAN" }
        .groupBy { it.kategori }
        .mapValues { (_, daftar) -> daftar.sumOf { it.jumlah } }
    println("Ringkasan per kategori:")
    ringkasanPerKategori.forEach { (k, v) ->
        println("  $k: $v")
    }

    // 3. Immutability — data class copy, bukan mutasi.
    val asli = Transaksi("Makanan", 50_000, "PENGELUARAN")
    val diperbarui = asli.copy(jumlah = asli.jumlah + 5_000)
    println("Asli: $asli")
    println("Diperbarui: $diperbarui")
    println("Asli tetap: $asli")

    // 4. Function composition untuk pipeline transformasi.
    val ambilJumlah: (Transaksi) -> Int = { it.jumlah }
    val diskon10persen: (Int) -> Int = { (it * 0.9).toInt() }
    val tambahPajak: (Int) -> Int = { (it * 1.11).toInt() }

    val proses = ambilJumlah andThen diskon10persen andThen tambahPajak
    val contoh = Transaksi("Buku", 100_000, "PENGELUARAN")
    println("Hasil transformasi: ${proses(contoh)}")

    // 5. Reduce/fold untuk akumulasi.
    val daftarAngka = listOf(1, 2, 3, 4, 5)
    val total = daftarAngka.fold(0) { acc, n -> acc + n }
    val produk = daftarAngka.fold(1) { acc, n -> acc * n }
    println("Total: $total, Produk: $produk")
}

// Helper infix untuk function composition.
infix fun <A, B, C> ((A) -> B).andThen(next: (B) -> C): (A) -> C {
    return { x -> next(this(x)) }
}

Output yang Dihasilkan:

text
Total pengeluaran makanan: 80000
Ringkasan per kategori:
  Makanan: 80000
  Transport: 25000
  Hiburan: 75000
Asli: Transaksi(kategori=Makanan, jumlah=50000, tipe=PENGELUARAN)
Diperbarui: Transaksi(kategori=Makanan, jumlah=55000, tipe=PENGELUARAN)
Asli tetap: Transaksi(kategori=Makanan, jumlah=50000, tipe=PENGELUARAN)
Hasil transformasi: 99900
Total: 15, Produk: 120

Mari kita telaah baris-baris kode di atas:

  1. Baris 6 (data class Transaksi(val kategori: String, val jumlah: Int, val tipe: String)): data class dengan property val — immutable. Tidak ada var, sehingga instance Transaksi tidak dapat dimutasi setelah dibuat.
  2. Baris 16-20 (val totalPengeluaranMakanan = transaksi.filter { ... }.map { ... }.fold(0) { ... }): pipeline map/filter/reduce. filter menyaring transaksi pengeluaran makanan, map mengambil property jumlah, fold menjumlahkan dari accumulator 0. Tidak ada loop manual atau mutasi.
  3. Baris 23-27 (transaksi.filter { ... }.groupBy { ... }.mapValues { ... }): group-by + agregasi. filter menyaring pengeluaran, groupBy mengelompokkan per kategori menjadi Map<String, List<Transaksi>>, mapValues mengubah setiap value menjadi total jumlah. Hasil adalah Map<String, Int>.
  4. Baris 30-33 (val diperbarui = asli.copy(jumlah = asli.jumlah + 5_000)): immutability via copy. asli tetap tidak berubah; copy membuat instance baru dengan jumlah yang dimodifikasi. Pola ini adalah inti immutability di Kotlin.
  5. Baris 36-42 (val ambilJumlah = ...; val diskon10persen = ...; val tambahPajak = ...): tiga function kecil, masing-masing satu tanggung jawab. Function-function ini akan digabung menjadi pipeline.
  6. Baris 44 (val proses = ambilJumlah andThen diskon10persen andThen tambahPajak): function composition. Operator andThen menggabungkan tiga function menjadi satu function (Transaksi) -> Int. Pemanggilan proses(contoh) menjalankan ketiganya secara berurutan.
  7. Baris 48-50 (val total = daftarAngka.fold(0) { ... }; val produk = daftarAngka.fold(1) { ... }): fold untuk akumulasi. fold menerima initial value dan lambda (acc, n) -> acc. Total memakai initial 0 dan operasi +; produk memakai initial 1 dan operasi *.

Immutability dengan val dan copy #

Immutability adalah prinsip yang didukung Kotlin melalui val dan data class copy. Alih-alih memutasi property, kita membuat instance baru dengan perubahan yang diinginkan.

kotlin Immutability pattern
data class Pengguna(val nama: String, val umur: Int)

val asli = Pengguna("Budi", 25)
val ultah = asli.copy(umur = 26)
// asli tetap Pengguna("Budi", 25) — tidak berubah

Manfaat immutability: aman dipakai di coroutine tanpa race condition, mudah diuji, dan history perubahan dapat dilacak.

Kotlin vs Haskell #

Kotlin bukan bahasa fungsional murni seperti Haskell. Berikut perbandingan singkat:

AspekKotlinHaskell
ParadigmaMulti-paradigm (OOP + FP)Pure functional
ImmutabilityDefault dengan valDefault untuk semua
Side effectDiizinkanDibatasi via IO monad
Lazy evaluationMemerlukan SequenceDefault
Pattern matchingwhen expressionBawaan, lebih ekspresif

Side-Effect-Free Pipeline #

Pola pipeline fungsional bersih dari side effect — setiap langkah hanya menghasilkan output baru tanpa mengubah input atau state luar:

kotlin Pipeline side-effect-free
val hasil = transaksi
    .filter { it.tipe == "PENGELUARAN" }
    .map { it.jumlah }
    .sortedDescending()
    .take(3)
    .sum()
// Tidak ada variabel yang dimutasi; transaksi tetap utuh.

Pipeline seperti ini mudah diuji, mudah diparalelkan, dan kebal terhadap bug mutasi.

Referensi Lanjutan #

Penutup #

Pola desain fungsional — map/filter/reduce, function composition, immutability, dan side-effect-free pipeline — membuat kode Kotlin lebih modular, mudah diuji, dan kebal terhadap bug akibat mutasi state. Walaupun Kotlin bukan bahasa fungsional murni seperti Haskell, kita dapat mengadopsi prinsip fungsional pada bagian kode yang paling membutuhkan kejelasan dan keandalan. Dengan menguasai lambdas, scope functions, dan pola fungsional, kita siap menulis kode Kotlin modern yang ringkas, ekspresif, dan andal di produksi.