Higher-order function và lambda trong Kotlin
1. Tổng quan
Trong Kotlin, function là “công dân hạng nhất”. Bạn có thể gán function vào biến, truyền nó làm tham số, hoặc trả nó về từ một function khác. Function nhận hoặc trả về function khác được gọi là higher-order function. Cách viết ngắn gọn nhất để tạo một function như vậy là lambda.
Bài này đi qua function type, cú pháp lambda, function reference, closure, lambda with receiver và từ khóa inline. Đọc xong, bạn sẽ tự viết được higher-order function của riêng mình. Bạn cũng sẽ biết tránh vài lỗi hay gặp.
2. Function type
Mỗi function trong Kotlin có một kiểu, gọi là function type. Cú pháp gồm danh sách kiểu tham số trong ngoặc, mũi tên, rồi kiểu trả về:
fun main() {
val add: (Int, Int) -> Int = { a, b -> a + b }
val greet: (String) -> Unit = { name -> println("Xin chào, $name") }
val now: () -> Long = { System.currentTimeMillis() }
println(add(2, 3))
greet("Lan")
println(now() > 0)
}
Giải thích:
(Int, Int) -> Intlà function nhận haiIntvà trả vềInt.(String) -> Unitlà function không trả giá trị có ý nghĩa, tương tựvoidtrong Java.() -> Longlà function không có tham số.- Bạn gọi biến function như gọi function thường:
add(2, 3). Cách này tương đươngadd.invoke(2, 3).
5
Xin chào, Lan
true
Function type cũng có thể nullable. Khi đó bạn phải bọc cả kiểu trong ngoặc: ((Order) -> Unit)?. Để gọi an toàn, bạn dùng callback?.invoke(order).
3. Viết higher-order function đầu tiên
Ta dùng một model đơn giản xuyên suốt bài. Số tiền dùng kiểu Long (đơn vị đồng) để tránh sai số dấu phẩy động:
data class Order(val id: String, val customer: String, val amount: Long)
fun calculateTotal(orders: List<Order>, priceOf: (Order) -> Long): Long {
var total = 0L
for (order in orders) {
total += priceOf(order)
}
return total
}
fun main() {
val orders = listOf(
Order("A1", "Lan", 100_000),
Order("A2", "Minh", 250_000),
Order("A3", "Lan", 50_000),
)
val full = calculateTotal(orders) { it.amount }
val discounted = calculateTotal(orders) { it.amount * 90 / 100 }
println(full)
println(discounted)
}
calculateTotal là một higher-order function vì tham số priceOf có kiểu (Order) -> Long. Function này không biết cách tính giá của từng đơn. Nó giao việc đó cho người gọi. Nhờ vậy, cùng một vòng lặp có thể dùng cho giá gốc lẫn giá đã giảm.
400000
360000
4. Cú pháp lambda
4.1. Các dạng viết
Lambda luôn nằm trong cặp {}. Tham số đứng trước ->, phần thân đứng sau. Biểu thức cuối cùng chính là giá trị trả về, bạn không cần viết return:
val isLarge: (Order) -> Boolean = { order ->
val threshold = 200_000
order.amount > threshold // giá trị trả về
}
val isLargeShort: (Order) -> Boolean = { it.amount > 200_000 }
val ignoreSecond: (Order, Int) -> String = { order, _ -> order.id }
- Khi lambda chỉ có một tham số, bạn có thể bỏ khai báo và dùng tên ngầm định
it. - Tham số không dùng tới nên đặt là
_để code rõ ý và tránh cảnh báo của compiler.
4.2. Trailing lambda
Nếu tham số cuối cùng của function là function type, bạn được đặt lambda ra ngoài dấu ngoặc tròn. Nếu đó là tham số duy nhất, bạn bỏ luôn cặp ():
val orders = listOf(
Order("A1", "Lan", 100_000),
Order("A2", "Minh", 250_000),
)
val large = orders.filter { it.amount > 200_000 }
val total = calculateTotal(orders) { it.amount }
val sorted = orders.sortedByDescending { it.amount }.map { it.id }
println(large.map { it.id })
println(sorted)
[A2]
[A2, A1]
Đây là lý do code Kotlin dùng collection API đọc gần như ngôn ngữ tự nhiên. Toàn bộ filter, map, sortedByDescending trong thư viện chuẩn đều là higher-order function.
5. Function reference
Khi đã có sẵn một function đúng chữ ký, bạn không cần bọc nó trong lambda. Hãy dùng toán tử :::
fun isVip(order: Order): Boolean = order.amount >= 200_000
class TaxService(private val ratePercent: Long) {
fun withTax(order: Order): Long = order.amount * (100 + ratePercent) / 100
}
fun main() {
val orders = listOf(Order("A1", "Lan", 100_000), Order("A2", "Minh", 250_000))
val taxService = TaxService(10)
println(orders.filter(::isVip).map { it.id }) // top-level function
println(orders.map(Order::amount)) // property reference
println(calculateTotal(orders, taxService::withTax)) // bound reference
}
::isViptham chiếu tới một function top-level.Order::amounttham chiếu tới property. Nó có kiểu(Order) -> Long.taxService::withTaxlà bound reference: function đã gắn sẵn với đối tượngtaxService.
[A2]
[100000, 250000]
385000
Function reference thường dễ đọc hơn lambda khi tên function đã nói rõ ý định.
6. Trả về function và closure
6.1. Function trả về function
Higher-order function cũng có thể trả về một function. Điều này hữu ích khi bạn muốn tạo ra hành vi dựa trên cấu hình:
fun discountFor(level: String): (Long) -> Long {
val percent = when (level) {
"GOLD" -> 80
"SILVER" -> 90
else -> 100
}
return { amount -> amount * percent / 100 }
}
fun main() {
val goldPrice = discountFor("GOLD")
val normalPrice = discountFor("NONE")
println(goldPrice(500_000))
println(normalPrice(500_000))
}
400000
500000
Lambda được trả về vẫn “nhớ” biến percent, dù discountFor đã chạy xong. Cơ chế này gọi là closure.
Lưu ý: Đừng viết
"GOLD" -> { amount -> amount * 80 / 100 }bên trongwhen. Ở vị trí đó, cặp{}được hiểu là một block, không phải lambda, nên code sẽ không compile. Hãy tính giá trị trước rồi trả về lambda như ví dụ trên. Bạn cũng có thể bọc lambda trong ngoặc tròn:({ amount -> ... }).
6.2. Closure có thể sửa biến
Khác với lambda trong Java, lambda Kotlin được phép thay đổi biến var mà nó capture:
fun orderIdGenerator(prefix: String): () -> String {
var next = 1
return { "$prefix-${next++}" }
}
fun main() {
val nextId = orderIdGenerator("ORD")
println(nextId())
println(nextId())
println(nextId())
}
ORD-1
ORD-2
ORD-3
Compiler bọc next trong một đối tượng tham chiếu, gọi là Ref, để lambda và function ngoài cùng chia sẻ một giá trị.
Lưu ý: Closure sửa biến dùng chung không thread-safe. Nếu nhiều thread cùng gọi
nextId(), bạn có thể nhận ID trùng nhau. Khi đó hãy dùngAtomicInteger.
7. Lambda with receiver
Kotlin có một dạng function type đặc biệt: T.() -> R. Bên trong lambda này, this chính là đối tượng kiểu T. Bạn truy cập trực tiếp member của nó mà không cần tiền tố. Đây là nền tảng của các DSL như buildString, apply, hay Gradle Kotlin DSL:
class OrderBuilder {
var id: String = ""
var customer: String = ""
var amount: Long = 0
fun build(): Order = Order(id, customer, amount)
}
fun order(block: OrderBuilder.() -> Unit): Order =
OrderBuilder().apply(block).build()
fun main() {
val o = order {
id = "B1"
customer = "Hoa"
amount = 120_000
}
println(o)
}
Order(id=B1, customer=Hoa, amount=120000)
block có kiểu OrderBuilder.() -> Unit. Vì vậy trong lambda, id = "B1" thực chất là this.id = "B1". apply(block) gọi lambda với receiver là builder vừa tạo, rồi trả về chính builder đó.
8. inline và return trong lambda
8.1. Chi phí của lambda
Trên JVM, mỗi lambda thường được biên dịch thành một đối tượng. Nếu lambda capture biến, mỗi lần gọi có thể sinh thêm một object mới. Với function được gọi rất nhiều lần, chi phí này cộng dồn lại đáng kể.
Đánh dấu function là inline sẽ giải quyết vấn đề đó. Compiler chép thân function và thân lambda thẳng vào chỗ gọi, nên không tạo object nào:
inline fun <T> measure(label: String, block: () -> T): T {
val start = System.nanoTime()
val result = block()
println("$label: ${(System.nanoTime() - start) / 1_000} µs")
return result
}
fun main() {
val orders = (1..1_000).map { Order("O$it", "C$it", it * 1_000L) }
val total = measure("Tính tổng") { calculateTotal(orders) { it.amount } }
println(total)
}
Tính tổng: 412 µs
500500000
Thời gian đo được sẽ khác nhau trên mỗi máy. filter, map, forEach trong thư viện chuẩn đều là inline.
8.2. Non-local return và label
Trong lambda truyền vào function inline, lệnh return trơn sẽ thoát khỏi function bao ngoài, không chỉ khỏi lambda:
fun findFirstLarge(orders: List<Order>, threshold: Long): Order? {
orders.forEach {
if (it.amount > threshold) return it // thoát khỏi findFirstLarge
}
return null
}
fun printPaidOrders(orders: List<Order>) {
orders.forEach {
if (it.amount == 0L) return@forEach // chỉ bỏ qua phần tử này
println(it.id)
}
println("Xong")
}
return itlà non-local return. Nó kết thúc luônfindFirstLarge.return@forEachlà return có label. Nó chỉ kết thúc lần chạy hiện tại của lambda, giốngcontinue.
Với function không inline, return trơn trong lambda sẽ bị compiler báo lỗi. Bạn bắt buộc phải dùng label.
Lưu ý: Nếu function
inlinetruyền lambda sang một ngữ cảnh khác, ví dụ chạy nó trong thread khác, hãy đánh dấu tham số làcrossinlineđể cấm non-local return. Nếu muốn lưu lambda vào biến hoặc field, hãy dùngnoinline.
9. Kiểm thử higher-order function
Thêm dependency test vào pom.xml. Dự án cần cấu hình sẵn kotlin-maven-plugin:
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<scope>test</scope>
</dependency>
</dependencies>
Higher-order function rất dễ test. Bạn chỉ cần truyền vào một lambda đơn giản và kiểm tra kết quả:
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class PricingTest {
private val orders = listOf(
Order("A1", "Lan", 100_000),
Order("A2", "Minh", 250_000),
Order("A3", "Lan", 50_000),
)
@Test
fun `calculateTotal applies the given price function`() {
assertThat(calculateTotal(orders) { it.amount }).isEqualTo(400_000L)
assertThat(calculateTotal(orders) { 1L }).isEqualTo(3L)
}
@Test
fun `discountFor returns function matching customer level`() {
assertThat(discountFor("GOLD")(500_000)).isEqualTo(400_000L)
assertThat(discountFor("SILVER")(500_000)).isEqualTo(450_000L)
assertThat(discountFor("UNKNOWN")(500_000)).isEqualTo(500_000L)
}
@Test
fun `generator keeps its own state per closure`() {
val first = orderIdGenerator("ORD")
val second = orderIdGenerator("INV")
assertThat(listOf(first(), first(), second()))
.containsExactly("ORD-1", "ORD-2", "INV-1")
}
}
Test thứ ba xác nhận một điểm quan trọng: mỗi lần gọi orderIdGenerator tạo ra một closure riêng, với biến next riêng.
10. Lỗi hay gặp và khi nào không nên dùng
- Lambda quá dài. Nếu thân lambda vượt quá vài dòng, hãy tách nó thành function có tên rồi truyền bằng
::. Code sẽ dễ đọc và dễ test hơn. - Lạm dụng
itlồng nhau. Nếu bạn viếtorders.map { it.items.filter { it.price > 0 } },itbên trong che mấtitbên ngoài. Hãy đặt tên tham số rõ ràng. inlinemọi thứ. Chỉ nên inline function nhỏ và có tham số là lambda. Inline function lớn làm phình bytecode. Inline function không có lambda gần như không đem lại lợi ích, và compiler sẽ cảnh báo bạn.- Nhầm
return. Mộtreturntrơn trongforEachthoát cả function bao ngoài. Đây là nguồn gốc của nhiều bug khó thấy. Khi chỉ muốn bỏ qua phần tử, hãy dùngreturn@forEach. - Capture đối tượng nặng. Lambda được lưu lâu dài, ví dụ làm listener, sẽ giữ tham chiếu tới mọi biến nó capture. Điều này có thể gây rò rỉ bộ nhớ, ví dụ khi giữ cả
Activitytrên Android. - Không cần trừu tượng hóa. Nếu chỉ có một cách xử lý duy nhất và nó không đổi, một function thường là đủ. Đừng thêm tham số lambda chỉ để “phòng khi cần”.
11. Kết luận
Trong bài này, bạn đã thấy function type mô tả chữ ký của function. Lambda và function reference là hai cách tạo giá trị function. Higher-order function nhận hoặc trả về function để tách phần “khung” khỏi phần “hành vi”. Bạn cũng đã làm quen với closure, lambda with receiver để xây DSL, và inline để loại bỏ chi phí object cùng cách non-local return hoạt động.
Khi nắm vững các khái niệm này, bạn sẽ đọc thư viện chuẩn Kotlin dễ dàng hơn nhiều. Bạn cũng sẽ biết lúc nào nên viết API dạng higher-order cho code của mình.