Higher-order function e lambda em Kotlin
1. Tópico geral
Em Kotlin, function é um “cidadão de primeira classe”. Você pode atribuir uma function a uma variável, passá-la como parâmetro ou retorná-la de outra function. Function que recebe ou retorna outra function é chamada de higher-order function. A forma mais concisa de criar tal function é usando lambda.
Este artigo aborda function type, sintaxe de lambda, function reference, closure, lambda with receiver e a palavra-chave inline. Depois de ler, você será capaz de escrever suas próprias higher-order functions. Você também saberá como evitar alguns erros comuns.
2. Function type
Cada function em Kotlin tem um tipo, chamado function type. A sintaxe consiste em uma lista dos tipos de parâmetros entre parênteses, uma seta, depois o tipo de retorno:
fun main() {
val add: (Int, Int) -> Int = { a, b -> a + b }
val greet: (String) -> Unit = { name -> println("Olá, $name") }
val now: () -> Long = { System.currentTimeMillis() }
println(add(2, 3))
greet("Lan")
println(now() > 0)
}
Explicação:
(Int, Int) -> Inté uma function que recebe doisInte retorna umInt.(String) -> Unité uma function que não retorna um valor significativo, similar avoidem Java.() -> Longé uma function sem parâmetros.- Você chama a variável function como chamaria uma function comum:
add(2, 3). Isso é equivalente aadd.invoke(2, 3).
5
Olá, Lan
true
O function type também pode ser nullable. Então você tem que envolver todo o tipo entre parênteses: ((Order) -> Unit)?. Para chamá-lo com segurança, você usa callback?.invoke(order).
3. Escrevendo a primeira higher-order function
Usamos um modelo simples ao longo do artigo. O valor usa o tipo Long (unidade: dong) para evitar erros de ponto flutuante:
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 é uma higher-order function porque o parâmetro priceOf tem o tipo (Order) -> Long. Essa function não sabe como calcular o preço de cada pedido. Ela delega este trabalho para quem a chama. Graças a isso, o mesmo loop pode ser usado tanto para o preço original quanto para o preço com desconto.
400000
360000
4. Sintaxe lambda
4.1. Diferentes formas de escrita
Lambda sempre está dentro de {}. Os parâmetros vêm antes de ->, o corpo vem depois. A última expressão é o valor retornado, você não precisa escrever return:
val isLarge: (Order) -> Boolean = { order ->
val threshold = 200_000
order.amount > threshold // valor retornado
}
val isLargeShort: (Order) -> Boolean = { it.amount > 200_000 }
val ignoreSecond: (Order, Int) -> String = { order, _ -> order.id }
- Quando a lambda tem apenas um parâmetro, você pode omitir a declaração e usar o nome implícito
it. - Parâmetros não usados devem ser nomeados
_para tornar o código claro e evitar avisos do compilador.
4.2. Trailing lambda
Se o último parâmetro de uma function for um function type, você pode colocar a lambda fora dos parênteses. Se for o único parâmetro, você omite os () completamente:
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]
Por isso o código Kotlin da collection API lê-se quase como linguagem natural. Todas as filter, map, sortedByDescending na biblioteca padrão são higher-order functions.
5. Function reference
Quando você já tem uma function com a assinatura correta, você não precisa envolvê-la em uma lambda. Use o operador :::
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
}
::isVipreferencia uma function de nível superior.Order::amountreferencia uma propriedade. Ela tem o tipo(Order) -> Long.taxService::withTaxé uma bound reference: a function já está ligada ao objetotaxService.
[A2]
[100000, 250000]
385000
Function reference é frequentemente mais fácil de ler que lambda quando o nome da function já deixa claro a intenção.
6. Retornando functions e closure
6.1. Function retornando function
Higher-order function também pode retornar uma function. Isso é útil quando você quer criar comportamento baseado em configuração:
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
A lambda retornada ainda “lembra” da variável percent, mesmo depois que discountFor terminou de executar. Este mecanismo é chamado de closure.
Observação: Não escreva
"GOLD" -> { amount -> amount * 80 / 100 }dentro dewhen. Nessa posição, os{}são entendidos como um bloco, não uma lambda, então o código não vai compilar. Calcule o valor primeiro, depois retorne a lambda como no exemplo acima. Você também pode envolver a lambda entre parênteses:({ amount -> ... }).
6.2. Closure pode modificar variáveis
Diferente de lambda em Java, lambda Kotlin é permitida modificar a variável var que ela captura:
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
O compilador envolve next em um objeto de referência, chamado Ref, para que a lambda e a function externa compartilhem um único valor.
Observação: Closure modificando variável compartilhada não é thread-safe. Se múltiplas threads chamarem
nextId(), você pode obter IDs duplicados. Então useAtomicInteger.
7. Lambda with receiver
Kotlin tem uma forma especial de function type: T.() -> R. Dentro dessa lambda, this é o objeto do tipo T. Você acessa seus membros diretamente sem um prefixo. Esta é a base de DSLs como buildString, apply ou 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 tem o tipo OrderBuilder.() -> Unit. Portanto, dentro da lambda, id = "B1" é na verdade this.id = "B1". apply(block) chama a lambda com o builder recém-criado como receiver, depois retorna o próprio builder.
8. inline e return em lambda
8.1. Custo da lambda
Na JVM, cada lambda geralmente é compilada para um objeto. Se a lambda captura variáveis, cada chamada pode gerar um novo objeto. Com functions chamadas muito frequentemente, esse custo se acumula.
Marcando uma function como inline resolve este problema. O compilador copia o corpo da function e o corpo da lambda diretamente para o ponto de chamada, então nenhum objeto é criado:
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("Calcular total") { calculateTotal(orders) { it.amount } }
println(total)
}
Calcular total: 412 µs
500500000
O tempo de medição será diferente em cada máquina. filter, map, forEach na biblioteca padrão são todos inline.
8.2. Non-local return e label
Em uma lambda passada para uma function inline, uma instrução return simples sai da function externa, não apenas da lambda:
fun findFirstLarge(orders: List<Order>, threshold: Long): Order? {
orders.forEach {
if (it.amount > threshold) return it // sai de findFirstLarge
}
return null
}
fun printPaidOrders(orders: List<Order>) {
orders.forEach {
if (it.amount == 0L) return@forEach // pula apenas este elemento
println(it.id)
}
println("Pronto")
}
return ité um non-local return. Ele terminafindFirstLargeimediatamente.return@forEaché um labeled return. Ele apenas termina a execução atual da lambda, comocontinue.
Com uma function que não é inline, um return simples em lambda causará erro do compilador. Você deve usar um label.
Observação: Se uma function
inlinepassa uma lambda para outro contexto, como executá-la em uma thread diferente, marque o parâmetro comocrossinlinepara proibir non-local return. Se você quer salvar a lambda em uma variável ou field, usenoinline.
9. Testando higher-order functions
Adicione a dependência de teste ao pom.xml. O projeto precisa ter kotlin-maven-plugin configurado:
<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 functions são muito fáceis de testar. Você apenas precisa passar uma lambda simples e verificar o resultado:
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")
}
}
O terceiro teste confirma um ponto importante: cada chamada a orderIdGenerator cria uma closure separada, com sua própria variável next.
10. Erros comuns e quando não usar
- Lambda muito longa. Se o corpo da lambda exceder algumas linhas, extraia-o para uma function nomeada e passe-a usando
::. O código será mais fácil de ler e testar. - Abuso de
itaninhado. Se você escreverorders.map { it.items.filter { it.price > 0 } }, oitinterno obscurece oitexterno. Use nomes de parâmetros explícitos. - Inline tudo. Apenas faça inline de functions pequenas com parâmetros lambda. Functions inline grandes incham o bytecode. Functions inline sem lambda trazem quase nenhum benefício, e o compilador avisará você.
returnconfuso. Umreturnsimples emforEachsai de toda a function externa. Esta é a fonte de muitos bugs difíceis de ver. Quando você quer apenas pular um elemento, usereturn@forEach.- Capturando objetos pesados. Lambda armazenada a longo prazo, por exemplo como um listener, mantém referências a todas as variáveis que captura. Isso pode causar vazamento de memória, por exemplo mantendo toda a
Activityno Android. - Não há necessidade de abstração. Se há apenas uma forma de lidar e nunca muda, uma function comum é suficiente. Não adicione parâmetros lambda apenas para “estar preparado”.
11. Conclusão
Neste artigo, você viu como function type descreve a assinatura de uma function. Lambda e function reference são duas formas de criar um valor function. Higher-order function recebe ou retorna functions para separar o “framework” do “comportamento”. Você também se familiarizou com closure, lambda with receiver para construir DSLs, e inline para eliminar custo de object e como non-local return funciona.
Quando você dominar estes conceitos, encontrará a biblioteca padrão de Kotlin muito mais fácil de ler. Você também saberá quando escrever APIs higher-order para seu próprio código.