Higher-Order Functions

Higher-Order Functions #

Kotlin memperlakukan fungsi sebagai first-class citizen — artinya fungsi bisa disimpan dalam variabel, dioper sebagai argumen, dan dikembalikan dari fungsi lain. Ini bukan fitur akademis yang jarang terpakai; justru hampir semua API Kotlin Standard Library dibangun di atas konsep ini. Setiap kali kamu menulis .filter { it > 0 }, .map { it.nama }, atau ?.let { kirim(it) }, kamu sedang menggunakan higher-order function. Memahami cara kerjanya dari dalam membuka kemampuan untuk menulis abstraksi yang bersih, kode yang lebih ringkas, dan API yang ekspresif. Artikel ini membahas function types, lambda, anonymous functions, inline functions, dan pola functional programming yang idiomatik di Kotlin.

Function Types #

Di Kotlin, setiap fungsi memiliki tipe. Function type ditulis dengan format (ParameterTypes) -> ReturnType.

// Fungsi biasa
fun tambah(a: Int, b: Int): Int = a + b

// Function type-nya: (Int, Int) -> Int

// Variabel yang menyimpan fungsi
val operasi: (Int, Int) -> Int = ::tambah   // referensi ke fungsi
val kali: (Int, Int) -> Int = { a, b -> a * b }  // lambda

// Fungsi tanpa parameter: () -> Unit
val salam: () -> Unit = { println("Halo!") }

// Fungsi dengan satu parameter: (String) -> Boolean
val cekPanjang: (String) -> Boolean = { teks -> teks.length > 5 }

// Nullable function type — fungsi yang mungkin null
val opsional: ((Int) -> String)? = null

// Function type dengan receiver — mirip extension function
val gandakan: Int.() -> Int = { this * 2 }
println(5.gandakan())   // 10
flowchart TD
    A["Function Type"] --> B["(ParameterTypes) -> ReturnType"]
    B --> C["Tanpa parameter\n() -> Unit"]
    B --> D["Satu parameter\n(String) -> Boolean"]
    B --> E["Banyak parameter\n(Int, Int) -> Int"]
    B --> F["Nullable\n((Int) -> String)?"]
    B --> G["Dengan receiver\nInt.() -> Int"]

Memanggil Function Type #

val hitung: (Int, Int) -> Int = { a, b -> a + b }

// Dua cara memanggil
val hasil1 = hitung(3, 4)       // cara biasa: 7
val hasil2 = hitung.invoke(3, 4) // eksplisit invoke: 7

// Nullable function type harus dicek sebelum dipanggil
val callback: (() -> Unit)? = null
callback?.invoke()   // aman — tidak crash jika null

Lambda Expressions #

Lambda adalah cara paling umum untuk mendefinisikan fungsi anonim di tempat. Lambda ditulis dalam kurung kurawal { }.

Sintaks Lambda #

// Sintaks lengkap
val tambah: (Int, Int) -> Int = { a: Int, b: Int -> a + b }

// Tipe bisa disimpulkan dari deklarasi variabel
val tambah: (Int, Int) -> Int = { a, b -> a + b }

// Atau tipe di parameter, tanpa anotasi di variabel
val tambah = { a: Int, b: Int -> a + b }

// Lambda satu parameter: gunakan 'it'
val kuadrat: (Int) -> Int = { it * it }
val kuadrat = { n: Int -> n * n }  // tanpa anotasi variabel

// Lambda tanpa parameter
val sapa: () -> Unit = { println("Halo!") }

// Lambda multi-baris — nilai terakhir adalah return value
val prosesAngka: (Int) -> String = { angka ->
    val doubled = angka * 2
    val formatted = "Hasil: $doubled"
    formatted   // ini yang dikembalikan, tanpa 'return'
}

Trailing Lambda #

Kotlin memiliki konvensi penting: jika parameter terakhir sebuah fungsi adalah function type, lambda bisa ditulis di luar kurung.

// Tanpa trailing lambda (verbose)
daftar.filter({ it > 0 })
daftar.map({ it * 2 })

// Dengan trailing lambda (idiomatik)
daftar.filter { it > 0 }
daftar.map { it * 2 }

// Saat ada parameter lain sebelum lambda
daftar.fold(0, { acc, n -> acc + n })        // tanpa trailing
daftar.fold(0) { acc, n -> acc + n }         // dengan trailing

// Jika lambda satu-satunya argumen, kurung bisa dihilangkan
run({ println("Halo") })   // dengan kurung
run { println("Halo") }    // trailing lambda — lebih bersih

// Contoh dengan fungsi kustom
fun ulangi(kali: Int, aksi: () -> Unit) {
    for (i in 0 until kali) aksi()
}

ulangi(3, { println("Halo") })   // tanpa trailing
ulangi(3) { println("Halo") }    // dengan trailing — lebih bersih

it — Parameter Implisit #

Saat lambda hanya memiliki satu parameter, Kotlin menyediakan nama implisit it.

// Eksplisit
val panjang: (String) -> Int = { teks -> teks.length }

// Dengan it
val panjang: (String) -> Int = { it.length }

// it dalam higher-order function
listOf("apel", "jeruk", "mangga").filter { it.startsWith("a") }
listOf(1, 2, 3, 4, 5).map { it * it }

// ANTI-PATTERN: it dalam lambda bersarang — ambigu
daftar.map {
    it.nama.let {
        it.uppercase()  // it ini String, bukan elemen daftar
    }
}

// BENAR: beri nama eksplisit saat ada potensi ambiguitas
daftar.map { item ->
    item.nama.let { nama ->
        nama.uppercase()
    }
}

Higher-Order Functions #

Higher-order function adalah fungsi yang menerima fungsi sebagai parameter, mengembalikan fungsi, atau keduanya.

Fungsi sebagai Parameter #

// Fungsi yang menerima fungsi sebagai parameter
fun terapkan(angka: Int, operasi: (Int) -> Int): Int {
    return operasi(angka)
}

val hasil1 = terapkan(5) { it * 2 }        // 10
val hasil2 = terapkan(5) { it * it }        // 25
val hasil3 = terapkan(5, ::factorial)        // referensi ke fungsi lain

// Fungsi dengan dua function parameter
fun kombinasi(
    angka: Int,
    transformasi: (Int) -> Int,
    format: (Int) -> String
): String {
    val diubah = transformasi(angka)
    return format(diubah)
}

val hasil = kombinasi(
    angka = 42,
    transformasi = { it * 2 },
    format = { "Nilai: $it" }
)
// "Nilai: 84"

// Multiple callbacks — pola umum untuk async atau event
fun unduhData(
    url: String,
    onSukses: (String) -> Unit,
    onGagal: (Exception) -> Unit,
    onSelesai: () -> Unit
) {
    try {
        val data = fetch(url)
        onSukses(data)
    } catch (e: Exception) {
        onGagal(e)
    } finally {
        onSelesai()
    }
}

unduhData(
    url = "https://api.example.com/data",
    onSukses = { data -> tampilkan(data) },
    onGagal = { e -> log.error("Gagal: ${e.message}") },
    onSelesai = { sembunyikanLoading() }
)

Fungsi sebagai Return Value #

// Fungsi yang mengembalikan fungsi — factory function
fun buatPenambah(n: Int): (Int) -> Int {
    return { x -> x + n }
}

val tambah5 = buatPenambah(5)
val tambah10 = buatPenambah(10)

println(tambah5(3))    // 8
println(tambah10(3))   // 13

// Fungsi yang mengembalikan predikat
fun buatFilter(minimum: Int): (Int) -> Boolean {
    return { it >= minimum }
}

val lebihDari10 = buatFilter(10)
val lebihDari100 = buatFilter(100)

listOf(5, 15, 8, 120, 50).filter(lebihDari10)   // [15, 120, 50]
listOf(5, 15, 8, 120, 50).filter(lebihDari100)  // [120]

// Pemilih strategi — Strategy Pattern dengan higher-order function
fun buatFormatter(gaya: String): (Double) -> String = when (gaya) {
    "rupiah" -> { nilai -> "Rp ${"%,.0f".format(nilai)}" }
    "dolar"  -> { nilai -> "${"$%.2f".format(nilai)}" }
    "persen" -> { nilai -> "${(nilai * 100).toInt()}%" }
    else     -> { nilai -> nilai.toString() }
}

val formatRupiah = buatFormatter("rupiah")
val formatDolar = buatFormatter("dolar")

println(formatRupiah(15_000_000.0))   // Rp 15.000.000
println(formatDolar(15_000_000.0))    // $15000000.00

Closure #

Lambda di Kotlin adalah closure — ia bisa mengakses dan memodifikasi variabel dari scope yang melingkupinya. Ini berbeda dari Java di mana variabel yang ditangkap harus effectively final.

// Lambda menangkap variabel dari scope luar
fun buatCounter(): () -> Int {
    var hitungan = 0
    return {
        hitungan++   // memodifikasi variabel dari scope luar
        hitungan
    }
}

val counter1 = buatCounter()
val counter2 = buatCounter()   // counter terpisah

println(counter1())   // 1
println(counter1())   // 2
println(counter1())   // 3
println(counter2())   // 1 — counter2 independen dari counter1

// Closure dalam konteks praktis
fun buatAkumulator(nilai awal: Double = 0.0): Pair<(Double) -> Unit, () -> Double> {
    var total = awal
    val tambah: (Double) -> Unit = { total += it }
    val ambil: () -> Double = { total }
    return tambah to ambil
}

val (tambahBelanja, totalBelanja) = buatAkumulator()
tambahBelanja(150_000.0)
tambahBelanja(75_000.0)
tambahBelanja(200_000.0)
println(totalBelanja())   // 425000.0
Closure yang menangkap variabel mutable bisa menyebabkan efek samping yang tidak terduga, terutama dalam konteks concurrent. Saat menggunakan closure di coroutine atau thread berbeda, pastikan variabel yang ditangkap dilindungi dengan mekanisme sinkronisasi yang tepat, atau gunakan StateFlow/AtomicInteger sebagai gantinya.

Inline Functions #

Setiap kali kamu memanggil higher-order function dengan lambda, Kotlin membuat objek baru untuk lambda tersebut — ini menambah overhead memori dan function call. inline menginstruksikan compiler untuk menyalin isi fungsi (dan lambdanya) langsung ke call site, menghilangkan overhead ini.

Masalah Tanpa inline #

// Tanpa inline: setiap panggilan membuat objek lambda baru
fun <T> ukurWaktu(blok: () -> T): T {
    val mulai = System.currentTimeMillis()
    val hasil = blok()
    println("Waktu: ${System.currentTimeMillis() - mulai}ms")
    return hasil
}

// Di bytecode, ini kira-kira setara dengan:
// val lambda = object : Function0<T> { override fun invoke(): T = ... }
// ukurWaktu(lambda)
// — object baru dibuat setiap pemanggilan

Dengan inline #

// Dengan inline: isi fungsi dan lambda disalin ke call site
inline fun <T> ukurWaktu(blok: () -> T): T {
    val mulai = System.currentTimeMillis()
    val hasil = blok()
    println("Waktu: ${System.currentTimeMillis() - mulai}ms")
    return hasil
}

// Pemanggilan:
val hasil = ukurWaktu { hitungKompleks() }

// Compiler mengubahnya menjadi (kira-kira):
// val mulai = System.currentTimeMillis()
// val hasil = hitungKompleks()   // isi lambda disalin langsung
// println("Waktu: ${System.currentTimeMillis() - mulai}ms")
// — tidak ada object lambda yang dibuat

noinline dan crossinline #

// noinline: tandai lambda yang tidak boleh di-inline
// (misalnya karena disimpan atau dioper ke fungsi lain)
inline fun proses(
    aksi: () -> Unit,
    noinline callback: () -> Unit   // ini tidak di-inline
) {
    aksi()
    simpanCallback(callback)   // butuh referensi objek sungguhan
}

// crossinline: lambda boleh di-inline tapi tidak boleh pakai non-local return
inline fun jalankanAsync(crossinline blok: () -> Unit) {
    Thread {
        blok()   // di sini tidak boleh ada non-local return
    }.start()
}

Non-local Return #

Salah satu kemampuan unik lambda di inline function adalah non-local return — return di dalam lambda bisa menghentikan fungsi yang melingkupinya, bukan hanya lambdanya.

// Tanpa inline: return di lambda hanya keluar dari lambda
fun cariPertama(daftar: List<Int>, predikat: (Int) -> Boolean): Int? {
    daftar.forEach { angka ->
        if (predikat(angka)) return angka  // ERROR jika forEach bukan inline
    }
    return null
}

// forEach adalah inline, jadi return di sini menghentikan fungsi cariPertama
fun cariPertama(daftar: List<Int>, predikat: (Int) -> Boolean): Int? {
    daftar.forEach { angka ->
        if (predikat(angka)) return angka  // non-local return: keluar dari cariPertama
    }
    return null
}

// Contoh nyata
fun prosesHinggaDitemukan(daftar: List<String>): String {
    daftar.forEach { item ->
        if (item.startsWith("TARGET")) return item   // keluar dari prosesHinggaDitemukan
        println("Melewati: $item")
    }
    return "Tidak ditemukan"
}

Anonymous Functions #

Selain lambda, Kotlin mendukung anonymous function — fungsi tanpa nama yang ditulis dengan sintaks fun. Perbedaan utama: return di anonymous function selalu keluar dari anonymous function itu sendiri, bukan dari fungsi yang melingkupinya.

// Lambda
val kuadrat1: (Int) -> Int = { it * it }

// Anonymous function — sintaks lebih eksplisit
val kuadrat2 = fun(n: Int): Int { return n * n }
val kuadrat3 = fun(n: Int) = n * n   // versi expression

// Perbedaan return behavior
fun prosesLambda(daftar: List<Int>) {
    daftar.forEach {
        if (it == 3) return   // non-local return: keluar dari prosesLambda!
        println(it)
    }
    println("Selesai")   // tidak pernah dieksekusi jika ada elemen == 3
}

fun prosesAnonFunc(daftar: List<Int>) {
    daftar.forEach(fun(angka: Int) {
        if (angka == 3) return   // return dari anonymous function saja
        println(angka)
    })
    println("Selesai")   // selalu dieksekusi
}

prosesLambda(listOf(1, 2, 3, 4, 5))
// Output: 1, 2   (return pada 3, "Selesai" tidak muncul)

prosesAnonFunc(listOf(1, 2, 3, 4, 5))
// Output: 1, 2, 4, 5, Selesai   (3 dilewati, tapi terus jalan)

Function References #

Selain lambda, kamu bisa mereferensikan fungsi yang sudah ada menggunakan operator ::.

fun kaliDua(n: Int) = n * 2
fun cekPositif(n: Int) = n > 0
fun formatAngka(n: Int) = "Angka: $n"

val angka = listOf(-3, -1, 0, 2, 4, 7)

// Tanpa function reference
angka.filter { cekPositif(it) }.map { kaliDua(it) }.map { formatAngka(it) }

// Dengan function reference — lebih bersih
angka.filter(::cekPositif).map(::kaliDua).map(::formatAngka)

// Method reference pada instance
val teks = listOf("  apel  ", "  jeruk  ", "  mangga  ")
teks.map(String::trim)    // method reference pada tipe
teks.map { it.trim() }    // ekuivalen

// Constructor reference
data class Produk(val nama: String)
val namaProduk = listOf("Laptop", "Mouse", "Keyboard")
val produk = namaProduk.map(::Produk)
// [Produk("Laptop"), Produk("Mouse"), Produk("Keyboard")]

// Reference ke member function
data class User(val nama: String, val aktif: Boolean)
val users = listOf(User("Andi", true), User("Budi", false), User("Clara", true))

users.filter(User::aktif)          // reference ke property
users.map(User::nama)              // reference ke property

Membangun Abstraksi dengan Higher-Order Functions #

Higher-order functions memungkinkan kamu membangun abstraksi yang powerful dan reusable.

Retry Logic #

// Abstraksi retry: coba lagi jika gagal
fun <T> retry(
    kali: Int,
    jeda: Long = 0L,
    blok: () -> T
): T {
    var eksepsiTerakhir: Exception? = null
    repeat(kali) { percobaan ->
        try {
            return blok()
        } catch (e: Exception) {
            eksepsiTerakhir = e
            println("Percobaan ${percobaan + 1} gagal: ${e.message}")
            if (jeda > 0) Thread.sleep(jeda)
        }
    }
    throw eksepsiTerakhir!!
}

// Penggunaan
val data = retry(kali = 3, jeda = 1000L) {
    ambilDataDariApi()   // akan dicoba 3 kali jika gagal
}

Transaction Pattern #

// Abstraksi transaksi database
fun <T> transaksi(koneksi: Connection, blok: (Connection) -> T): T {
    koneksi.autoCommit = false
    return try {
        val hasil = blok(koneksi)
        koneksi.commit()
        hasil
    } catch (e: Exception) {
        koneksi.rollback()
        throw e
    } finally {
        koneksi.autoCommit = true
    }
}

// Penggunaan
val hasilTransfer = transaksi(db) { conn ->
    kurangiSaldo(conn, dariAkun, jumlah)
    tambahSaldo(conn, keAkun, jumlah)
    catatHistori(conn, dariAkun, keAkun, jumlah)
}

Pipeline dan Komposisi Fungsi #

// Komposisi fungsi: gabungkan dua fungsi menjadi satu
infix fun <A, B, C> ((A) -> B).then(g: (B) -> C): (A) -> C = { a -> g(this(a)) }

val bersihkan: (String) -> String = { it.trim() }
val besarkan: (String) -> String = { it.uppercase() }
val tambahPrefix: (String) -> String = { ">>> $it" }

val proses = bersihkan then besarkan then tambahPrefix

println(proses("  halo dunia  "))   // ">>> HALO DUNIA"

// Pipeline untuk transformasi data
fun <T> T.pipe(vararg transformasi: (T) -> T): T =
    transformasi.fold(this) { acc, fn -> fn(acc) }

val hasil = "  kotlin is fun  ".pipe(
    { it.trim() },
    { it.uppercase() },
    { it.replace(" ", "_") }
)
// "KOTLIN_IS_FUN"

Memoization #

// Cache hasil fungsi untuk input yang sama
fun <T, R> ((T) -> R).memoize(): (T) -> R {
    val cache = mutableMapOf<T, R>()
    return { input ->
        cache.getOrPut(input) { this(input) }
    }
}

// Fungsi rekursif Fibonacci tanpa memoization: O(2^n)
fun fibonacci(n: Int): Long = when (n) {
    0, 1 -> n.toLong()
    else -> fibonacci(n - 1) + fibonacci(n - 2)
}

// Dengan memoization: O(n)
val fibMemo: (Int) -> Long = { n: Int ->
    when (n) {
        0, 1 -> n.toLong()
        else -> fibMemo(n - 1) + fibMemo(n - 2)
    }
}.let { fn ->
    val cache = mutableMapOf<Int, Long>()
    { n: Int -> cache.getOrPut(n) { fn(n) } }
}

Pola Functional Programming Idiomatik #

Gunakan Fungsi Bawaan Daripada Loop Manual #

val karyawan = listOf(
    Karyawan("Andi", "Engineering", 15_000_000.0),
    Karyawan("Budi", "Marketing", 12_000_000.0),
    Karyawan("Clara", "Engineering", 18_000_000.0)
)

// ANTI-PATTERN: loop imperatif
var totalGaji = 0.0
for (k in karyawan) {
    if (k.departemen == "Engineering") {
        totalGaji += k.gaji
    }
}

// BENAR: functional style
val totalGaji = karyawan
    .filter { it.departemen == "Engineering" }
    .sumOf { it.gaji }

// ANTI-PATTERN: loop untuk mencari elemen
var karyawanTertinggi: Karyawan? = null
for (k in karyawan) {
    if (karyawanTertinggi == null || k.gaji > karyawanTertinggi!!.gaji) {
        karyawanTertinggi = k
    }
}

// BENAR:
val karyawanTertinggi = karyawan.maxByOrNull { it.gaji }

Hindari Side Effects di Transformasi #

// ANTI-PATTERN: side effect di dalam map
val hasil = mutableListOf<String>()
daftar.map { item ->
    hasil.add(item.nama)   // side effect dalam map!
    item.proses()
}

// BENAR: pisahkan transformasi dan efek samping
val diproses = daftar.map { it.proses() }
val namaSaja = daftar.map { it.nama }
diproses.forEach { simpan(it) }   // efek samping di forEach, bukan map

Gunakan takeIf dan takeUnless #

// takeIf: kembalikan objek jika kondisi terpenuhi, null jika tidak
// takeUnless: kebalikannya

// ANTI-PATTERN:
val input = editText.text.toString()
val validInput = if (input.isNotBlank()) input else null

// BENAR: takeIf lebih ekspresif
val validInput = editText.text.toString().takeIf { it.isNotBlank() }

// takeUnless: kembalikan objek jika kondisi TIDAK terpenuhi
val produkAktif = produk.takeUnless { it.dihapus }

// Kombinasi dengan let
editText.text.toString()
    .takeIf { it.isNotBlank() }
    ?.trim()
    ?.let { input -> simpanData(input) }

Kapan Menggunakan Higher-Order Functions #

Gunakan higher-order functions jika:
  ✓ Punya logika yang sama dengan variasi di "bagian tengah"
    (Template Method pattern → higher-order function)
  ✓ Butuh callback untuk operasi async atau event
  ✓ Ingin memisahkan what dari how (strategi yang bisa diganti)
  ✓ Membangun DSL atau fluent API
  ✓ Abstraksi resource management (open-use-close)

Hindari jika:
  ✗ Logika sederhana yang lebih jelas ditulis langsung
  ✗ Lambda terlalu panjang (> 10 baris) — ekstrak ke fungsi bernama
  ✗ Nesting lambda lebih dari dua level — refactor ke fungsi terpisah
  ✗ Tim tidak familiar dan kode jadi lebih sulit di-debug

Ringkasan #

  • Function type ditulis sebagai (ParamTypes) -> ReturnType. Fungsi adalah nilai di Kotlin — bisa disimpan dalam variabel, dioper sebagai argumen, dan dikembalikan dari fungsi lain.
  • Lambda ditulis dalam { }. Parameter implisit it tersedia saat hanya ada satu parameter. Gunakan nama eksplisit (item, user) saat lambda lebih dari satu baris atau ada nesting.
  • Trailing lambda: jika parameter terakhir fungsi adalah function type, lambda bisa ditulis di luar kurung — filter { } bukan filter({ }). Ini adalah konvensi Kotlin yang harus selalu diikuti.
  • Closure: lambda bisa menangkap dan memodifikasi variabel dari scope luar. Berbeda dari Java yang mengharuskan variabel yang ditangkap bersifat effectively final.
  • inline menghilangkan overhead object allocation lambda dengan menyalin isi fungsi ke call site. Gunakan untuk higher-order functions yang sering dipanggil di hot path.
  • Non-local return: return di dalam lambda dari inline function bisa menghentikan fungsi yang melingkupinya. Gunakan return@namaFungsi untuk return lokal yang eksplisit.
  • Anonymous functions (fun(x: Int) = x * 2) berperilaku berbeda dari lambda: return selalu lokal ke anonymous function itu sendiri, tidak pernah non-local.
  • Function references (::fungsi, Kelas::method) membuat kode lebih bersih ketika lambda hanya meneruskan argumen ke satu fungsi lain.
  • takeIf mengembalikan objek jika kondisi terpenuhi, null jika tidak. takeUnless adalah kebalikannya. Keduanya menggantikan pola if (kondisi) objek else null.
  • Higher-order functions adalah fondasi dari abstraksi powerful seperti retry logic, transaction pattern, pipeline komposisi, dan memoization — pola yang membuat kode lebih deklaratif dan reusable.

← Sebelumnya: Ranges & Progressions   Berikutnya: Type Conversion →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact