Pada standard library Kotlin sudah terdapat beberapa fungsi yang bisa kita manfaatkan untuk mengeksekusi sebuah scope code dalam konteks sebuah objek. Fungsi-fungsi ini disebut dengan scope functions. Dengan memanfaatkan scope functions, code yang kita buat akan lebih simpel(mengurangi boilerplate) dan readable. Tetapi sebelum kita bahas lebih lanjut topik scope function ini, saya asumsikan anda sudah paham beberapa topik seperti extensions function pada Kotlin, higher order function dan anonymous function/lambda.
Untuk sedikit lebih ada gambaran tentang extensions function, higher order function dan anonymous function/lambda pada Kotlin, kita akan bahas sedikit satu persatu topik tersebut.
First class citizen function dan higher order function
Function pada Kotlin by default adalah first-class-citizen seperti pada bahasa non fungsional murni seperti Go, Javascript, Rust. First-class-citizen yang berarti fungsi pada Kotlin bisa ditempatkan dimanapun, seperti didalam variabel, struktur data(List, Map, dsb), argument fungsi, kembalian nilai sebuah fungsi (return value). Topik first-class-citizen berhubungan erat dengan higher order function. Pengertian Higher order function sendiri adalah function yang minimal bisa meletakan fungsi lain pada parameternya atau mengembalikan fungsi lain pada return value-nya.
Berikut ini adalah contoh higher order function menggunakan built-in function filter. Pada contoh code dibawah ini, kita memanfaatkan built-in function filter untuk memfilter data list string yang berawalan dengan huruf b saja. Kemudian hasil filter tersebut ditampung pada variabel baru.
fun main() {
val datas = mutableListOf("Megatron", "Starscream", "Bonecrusher", "Barricade")
val pred: (x: String) -> Boolean = { x -> x.startsWith("b", true) }
val newDatas = datas.filter(pred)
println(newDatas)
}
Perhatikan baris ke 3, pada baris ke 3 kita membuat variabel pred, dimana variabel pred adalah sebuah variabel yang menampung value sebuah function. Sehingga fungsi yang ditampung pada variabel pred inilah yang bisa kita sebut first class citizen function.
Kemudian perhatikan baris ke 5, pada baris ke 5 kita menggunakan built-in function filter. Fungsi filter ini memiliki satu parameter yaitu sebuah fungsi. Sehingga fungsi filter ini bisa kita sebut dengan higher order function. Berikut adalah signature dari fungsi filter. Pada signature tersebut parameter satu fungsi dengan parameter generic T dan return type Boolean.
fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T>
Lambda expressions dan anonymous function
Contoh code yang sebelumnya kita sudah buat sebenarnya bisa kita sederhanakan lagi menggunakan lambda expressions maupun anonymous function.
Jika anda perlu menentukan tipe parameter dan tipe return-nya secara eksplisit, anda bisa menggunakan anonymous function.
fun main() {
val datas = mutableListOf("Megatron", "Starscream", "Bonecrusher", "Barricade")
val newDatas = datas.filter(fun(x: String): Boolean = x.startsWith("b", true))
println(newDatas)
}
filter dengan anonymous function
Anonymous function sebenarnya mirip seperti fungsi pada umumnya, bisa diperhatikan dia tetap menggunakan keyword fun. Hanya saja dia tidak memiliki nama.
Namun jika anda tidak perlu menentukan tipe parameter dan tipe return-nya secara eksplisit, anda bisa menggunakan lambda expressions seperti dibawah ini.
fun main() {
val datas = mutableListOf("Megatron", "Starscream", "Bonecrusher", "Barricade")
val newDatas = datas.filter { x -> x.startsWith("b", true) }
println(newDatas)
}
fungsi filter dengan lambda expressions.
Sifat dari lambda expression pada Kotlin:
- Lambda expressions selalu dibuka dan ditutup dengan curly bracket(
{
dan}
). - tipe data parameternya opsional .
- function body berada setelah tanda
->
.
Extension functions
Kotlin memberikan keleluasaan kepada kita untuk memberikan fungsionalitas baru pada sebuah class tanpa perlu membuatkan sub class baru dari class tersebut. Sebagai contoh ketika kita perlu membuatkan fungsi baru pada sebuah class yang berasal dari third party library. Hal ini bisa kita solve dengan extension function.
Contoh pertama dibawah ini ketika kita ingin membuatkan satu extension function logging sederhana pada variabel-variabel yang kita buat dengan tipe data string.
fun String.myValue() = "my value : $this"
fun main() {
val name = "wuriyanto"
println(name.myValue())
}
Contoh extension function pada String
Contoh kedua dibawah ini kita membuatkan satu extension function untuk mengalikan semua variabel dengan tipe data Int dengan 10. Extension function tersebut kita beri nama mulByTen.
fun Int.mulByTen() = this * 10
fun main() {
val i = 5
println(i.mulByTen())
}
Contoh extension function pada Int
Pada contoh ketiga ini kita akan membuat extension function pada class HttpServer yang kita anggap saja adalah class yang berasal dari third party library.
class HttpServer(private val port: Int) {
fun start() {
println("server running on port $port")
}
}
fun HttpServer.log(message: String) {
println("HTTP Server log: $message")
}
fun main() {
val server = HttpServer(8000)
server.start()
server.log("HTTP Server up and running")
}
Class HttpServer
Pada class HttpServer diatas kita menambahkan extension function log
. Ketika misalnya kita membutuhkan sebuah fungsionalitas log
pada class tersebut. Kita tidak perlu membuatkan sub class baru untuk class HttpServer yang kita bayangkan class tersebut berasal dari third party library.
Scope functions
Kita sudah membahas sedikit pengertian scope function pada bagian awal artikel ini. Scope function adalah built in function yang disediakan oleh Kotlin yang berguna untuk mengeksekusi suatu block code dalam konteks sebuah objek. Untuk memahami scope function pada Kotlin, anda perlu memahami topik seperti extensions function pada Kotlin, higher order function dan anonymous function/lambda yang sudah kita bahas sebelumnya.
Total ada 5 function yang termasuk scope function pada Kotlin. let
, run
, with
, apply
, dan also
. Lima scope function tersebut dibagi menjadi 2 kategori. Yaitu kategori mutation function, apply
dan also
. Kemudian kategori transformation function, let
, run
, dan with
. Kelima function tersebut hampir mirip secara penggunaan, hanya saja kita harus tau kapan dan dimana menggunakan masing-masing dari kelima function tersebut.
Context object, this dan it
this
dan it
adalah context object yang tersedia didalam lambda function pada setiap scope function. Context object this
dan it
bisa kita gunakan sebagai pengganti nama objek aslinya. Setiap scope function menggunakan salah satu dari this
dan it
untuk mengakses context object-nya.
this = T.()
adalah lambda receiver dan berada didalam lambda function yang berada pada extension function run
, with
, dan apply
. Sedangkan it = (T)
adalah lambda argument dan berada didalam lambda function yang berada pada extension function let
dan also
.
apply
apply
adalah scope function yang masuk kategori mutation function. Context object yang tersedia adalah this
. Return value dari apply
adalah objek dia sendiri. Sehingga apply
biasanya digunakan untuk menginisialisasi sebuah objek atau membuat class builder.
Signature:
fun <T> T.apply(block: T.() -> Unit): T
Jika kita perlu menggunakan keyword this
pada setiap property class-nya kita bisa tuliskan seperti dibawah ini.
data class Kaiju(var name: String, var category: String = "5")
fun main() {
val knifeHeadApply = Kaiju("Knife Head").apply {
this.category = "4"
}
println(knifeHeadApply)
}
apply dan data class dengan explicit this
Namun jika kita tidak perlu menuliskannya, maka keyword this
bisa kita kita omit dan ditulis seperti dibawah ini.
data class Kaiju(var name: String, var category: String = "5")
fun main() {
val knifeHeadApply = Kaiju("Knife Head").apply {
category = "4"
}
println(knifeHeadApply)
}
apply dan data class tanpa explicit this
Kita juga bisa memanfaatkan apply
untuk mengimplementasikan builder pattern.
data class Kaiju(var name: String = "", var category: String = "5", var height: Int = 0) {
fun name(pName: String): Kaiju = apply { name = pName }
fun category(pCategory: String): Kaiju = apply { category = pCategory }
fun height(pHeight: Int): Kaiju = apply { height = pHeight }
}
fun main() {
val knifeHeadApply = Kaiju()
.name("Knife Head")
.category("5")
.height(200)
println(knifeHeadApply)
}
apply dan builder pattern
also
also
adalah scope function yang masuk kategori mutation function. Context object yang tersedia adalah it
. Return value dari also
adalah objek dia sendiri. also
biasannya digunakan ketika kita butuh akses terhadap object reference-nya itu sendiri, bukan ketika butuh akses terhadap property atau method dari objek tersebut. Atau kita hanya butuh sekedar logic sederhana misalnya untuk logging value pada flow code yang berjalan.
Signature:
fun <T> T.also(block: (T) -> Unit): T
fun main() {
val name = "Cherno Alpha"
.also { println("original value : $it") }
.toUpperCase()
println(name)
}
menggunakan also untuk proses logging
Return value dari also
adalah objek dia sendiri. Sehingga kita bisa menggunakan also
untuk menginisialisasi sebuah objek seperti kita melakukannya ketika menggunakan apply
. Hanya saja code yang ditulis menjadi lebih verbose, karena secara eksplisit kita menuliskan context object it
.
data class Kaiju(var name: String = "", var category: String = "5", var height: Int = 0)
fun main() {
val knifeHeadApply = Kaiju().also {
it.name = "Knife Head"
it.category = "5"
it.height = 200
}
println(knifeHeadApply)
}
also untuk inisialisasi objek
let
let
adalah scope function yang masuk kategori transformation function. Context object yang tersedia adalah it
. Return value dari let
adalah result dari lambda dimasukan kedalam scope function-nya.
Signature:
fun <T, R> T.let(block: (T) -> R): R
Berikut contoh penggunaan let
untuk melakukan transformasi objek dengan tipe data tertentu ke objek dengan tipe data lain.
fun toByte(s: String): ByteArray {
return s.toByteArray(Charsets.UTF_8)
}
fun sendToNetworkAndDecode(d: ByteArray) {
println(d.toString(Charsets.UTF_8))
}
fun main() {
val name = "Leatherback"
println(name)
sendToNetworkAndDecode(name.let{ toByte(it) })
}
Pada contoh diatas kita menggunakan let
untuk proses transformasi variabel dari tipe data string ke tipe data bytes. Jika tidak menggunakan let
mungkin kita perlu membuat variabel baru seperti dibawah ini.
fun toByte(s: String): ByteArray {
return s.toByteArray(Charsets.UTF_8)
}
fun sendToNetworkAndDecode(d: ByteArray) {
println(d.toString(Charsets.UTF_8))
}
fun main() {
val name = "Leatherback"
println(name)
val nameBytes = toByte(name)
sendToNetworkAndDecode(nameBytes)
tanpa let
Atau kita bisa menyederhanakan lagi code let
dengan menggunakan method reference, dengan catatan fungsi code block hanya ada satu fungsi dengan parameter it
. Kita bisa lakukan seperti code dibawah ini.
fun toByte(s: String): ByteArray {
return s.toByteArray(Charsets.UTF_8)
}
fun sendToNetworkAndDecode(d: ByteArray) {
println(d.toString(Charsets.UTF_8))
}
fun main() {
val name = "Leatherback"
println(name)
sendToNetworkAndDecode(name.let(::toByte))
}
dengan method reference
run
run
adalah scope function yang masuk kategori transformation function. Context object yang tersedia adalah this
. Return value dari run
adalah result dari lambda dimasukan kedalam scope function-nya. run
tersedia dalam dua jenis, yaiturun
sebagai extension function danrun
sebagai non extension function. run
hampir sama dengan let
hanya saja run
menggunakan this
sebagai context object-nya.
run
sebagai extension function
Signature:
fun <T, R> T.run(block: T.() -> R): R
class Jaeger(var name: String,
var origin: String = "",
var victims: List<String> = mutableListOf("Yamarashi", "Otachi")) {
fun killCount(): Int {
return victims.size
}
}
fun main() {
val strikerEureka = Jaeger("Striker Eureka")
val totalVictim = strikerEureka.run {
origin = "Sidney, Australia"
killCount()
}
println(totalVictim)
}
run sebagai extension function
run
berguna ketika lambda yang kita provide terdapat inisialisasi objek dan komputasi yang menghasilkan return value seperti pada code diatas.
run
sebagai non extension function
Signature:
fun <R> run(block: () -> R): R
class Jaeger(var name: String,
var origin: String = "",
var victims: List<String> = mutableListOf("Yamarashi", "Otachi")) {
fun killCount(): Int {
return victims.size
}
}
fun main() {
val strikerEureka = Jaeger("Striker Eureka")
val totalVictim = run {
Jaeger("Striker Eureka",
victims = mutableListOf("Yamarashi", "Otachi", "Scunner"))
.killCount()
}
println(totalVictim)
}
run sebagai non extension function
run
sebagai non extension function berguna ketika kita membutuhkan beberapa statement sekaligus yang menghasilkan sebuah value.
with
with
adalah scope function yang masuk kategori transformation function. By default dia adalah non extension function. Context object yang tersedia adalah this
tetapi context object-nya diperlakukan sebagai argument. Return value dari run
adalah result dari lambda dimasukan kedalam scope function-nya.
Signature:
fun <T, R> with(receiver: T, block: T.() -> R): R
class Jaeger(var name: String,
var origin: String = "",
var victims: MutableList<String> = mutableListOf("Yamarashi", "Otachi")) {
fun kill(kaijuName: String) {
victims.add(kaijuName)
}
fun killCount(): Int {
return victims.size
}
}
fun main() {
val strikerEureka = Jaeger("Striker Eureka")
val totalVictim = with(strikerEureka) {
kill("Mutavore")
kill("Slattern")
killCount()
}
println(totalVictim)
}
contoh menggunakan with
with
berguna ketika kita ingin mengumpulkan pemanggilan function/method sebuah object kemudian mendapatkan value dari proses pemanggilan beberapa function/method dari object tersebut. Pada contoh diatas kita mengirim object strikerEureka
kedalam fungsi with
. Kemudian kita memanggil method kill
dua kali, kemudian kita memanggil fungsi killCount
untuk mendapatkan total jumlah victims dari strikerEureka
.
Tabel perbedaan dari setiap scope function

Kesimpulan
Kita sudah membahas apa itu scope function pada artikel ini. Karena secara penggunaan setiap scope function tersebut hampir mirip satu sama lain, ada baiknya kita lebih sering memanfaatkannya. Supaya kita bisa belajar dan lebih paham lagi kapan dan dimana menggunakannya.