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 dois Int e retorna um Int.
  • (String) -> Unit é uma function que não retorna um valor significativo, similar a void em Java.
  • () -> Long é uma function sem parâmetros.
  • Você chama a variável function como chamaria uma function comum: add(2, 3). Isso é equivalente a add.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
}
  • ::isVip referencia uma function de nível superior.
  • Order::amount referencia uma propriedade. Ela tem o tipo (Order) -> Long.
  • taxService::withTax é uma bound reference: a function já está ligada ao objeto taxService.
[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 de when. 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 use AtomicInteger.

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 termina findFirstLarge imediatamente.
  • return@forEach é um labeled return. Ele apenas termina a execução atual da lambda, como continue.

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 inline passa uma lambda para outro contexto, como executá-la em uma thread diferente, marque o parâmetro como crossinline para proibir non-local return. Se você quer salvar a lambda em uma variável ou field, use noinline.

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 it aninhado. Se você escrever orders.map { it.items.filter { it.price > 0 } }, o it interno obscurece o it externo. 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ê.
  • return confuso. Um return simples em forEach sai de toda a function externa. Esta é a fonte de muitos bugs difíceis de ver. Quando você quer apenas pular um elemento, use return@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 Activity no 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.

kotlinlambdahigher-order functionfunctional programming