Kotlin nasceu como uma linguagem multiparadigma, e um dos seus grandes trunfos é o suporte robusto à programação funcional sem abrir mão da orientação a objetos. Se você vem do Java, vai perceber que Kotlin torna o estilo funcional muito mais acessível e expressivo. Neste guia, vamos explorar cada conceito fundamental da programação funcional em Kotlin, com exemplos práticos que você pode aplicar no dia a dia dos seus projetos.

Funções como Cidadãs de Primeira Classe

Em Kotlin, funções são cidadãs de primeira classe. Isso significa que você pode armazenar funções em variáveis, passá-las como argumentos para outras funções e retorná-las como resultado. Esse conceito é a base de tudo que vamos ver ao longo deste guia.

val saudacao: (String) -> String = { nome -> "Ola, $nome!" }

fun main() {
    println(saudacao("Kotlin Brasil"))
    // Saída: Olá, Kotlin Brasil!

    val funcoes = listOf<(Int) -> Int>(
        { it * 2 },
        { it + 10 },
        { it * it }
    )

    funcoes.forEach { f -> println(f(5)) }
    // Saída: 10, 15, 25
}

Repare que a variável saudacao armazena uma lambda diretamente. Não existe nenhuma cerimônia extra – basta declarar o tipo funcional e atribuir o bloco de código. Isso já é radicalmente diferente do que você encontra no Java, onde seria necessário usar interfaces funcionais ou classes anônimas.

Funções de Alta Ordem

Uma função de alta ordem (higher-order function) é aquela que recebe outras funções como parâmetro ou retorna uma função. A biblioteca padrão do Kotlin está repleta delas: map, filter, fold, reduce, flatMap, entre muitas outras.

fun <T> List<T>.filtrarE(
    predicado1: (T) -> Boolean,
    predicado2: (T) -> Boolean
): List<T> {
    return this.filter { predicado1(it) && predicado2(it) }
}

fun main() {
    val numeros = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

    val resultado = numeros.filtrarE(
        predicado1 = { it > 3 },
        predicado2 = { it % 2 == 0 }
    )

    println(resultado) // [4, 6, 8, 10]
}

A função filtrarE recebe dois predicados e aplica ambos ao mesmo tempo. Esse padrão é muito útil para construir pipelines de filtragem flexíveis, especialmente quando os critérios vêm de fontes diferentes, como inputs do usuário ou configurações dinâmicas.

Lambdas e Sintaxe Concisa

Kotlin oferece uma sintaxe de lambda extremamente enxuta. Quando uma lambda tem apenas um parâmetro, você pode usar it como referência implícita. Além disso, se a lambda for o último argumento de uma função, ela pode ficar fora dos parênteses.

val nomes = listOf("Ana", "Bruno", "Carlos", "Diana")

// Sintaxe completa
val maiusculas1 = nomes.map({ nome: String -> nome.uppercase() })

// Tipo inferido
val maiusculas2 = nomes.map({ nome -> nome.uppercase() })

// Lambda fora dos parênteses
val maiusculas3 = nomes.map { nome -> nome.uppercase() }

// Usando it
val maiusculas4 = nomes.map { it.uppercase() }

// Referência de função
val maiusculas5 = nomes.map(String::uppercase)

Todas as cinco formas produzem o mesmo resultado. No dia a dia, as formas mais concisas (com it ou referência de função) são as preferidas pela comunidade. Se você quer se aprofundar em como lambdas se conectam com coroutines, confira o guia completo de coroutines.

Funções Puras e Imutabilidade

Uma função pura é aquela que, dados os mesmos argumentos, sempre retorna o mesmo resultado e não causa efeitos colaterais. Esse conceito está no coração da programação funcional e Kotlin facilita muito a sua adoção por meio da palavra-chave val e das coleções imutáveis.

// Função pura: sem efeitos colaterais, resultado previsível
fun calcularDesconto(preco: Double, percentual: Double): Double {
    return preco * (1.0 - percentual / 100.0)
}

// Uso de val para imutabilidade
data class Produto(val nome: String, val preco: Double)

fun aplicarDesconto(produtos: List<Produto>, percentual: Double): List<Produto> {
    return produtos.map { it.copy(preco = calcularDesconto(it.preco, percentual)) }
}

fun main() {
    val catálogo = listOf(
        Produto("Teclado", 250.0),
        Produto("Mouse", 120.0),
        Produto("Monitor", 1800.0)
    )

    val comDesconto = aplicarDesconto(catálogo, 10.0)
    println(comDesconto)
    // O catálogo original permanece intacto
    println(catálogo)
}

Repare que usamos copy da data class para criar novas instâncias em vez de modificar as existentes. A lista original catálogo nunca é alterada. Esse padrão de imutabilidade reduz drasticamente os bugs relacionados a estado compartilhado, especialmente em ambientes concorrentes.

Composição de Funções

Composição de funções é o ato de combinar funções simples para criar funções mais complexas. Em matemática, a composição de f e g é f(g(x)). Kotlin não tem um operador nativo de composição, mas é trivial criar um:

infix fun <A, B, C> ((B) -> C).comp(outra: (A) -> B): (A) -> C {
    return { a: A -> this(outra(a)) }
}

fun duplicar(x: Int): Int = x * 2
fun incrementar(x: Int): Int = x + 1
fun paraTexto(x: Int): String = "Resultado: $x"

fun main() {
    val pipeline = ::paraTexto comp ::duplicar comp ::incrementar

    println(pipeline(5))  // Resultado: 12
    // Execução: incrementar(5) = 6 -> duplicar(6) = 12 -> paraTexto(12)
}

A composição permite criar pipelines de transformação reutilizáveis. Você define cada etapa isoladamente, testa cada uma separadamente e depois as compõe. Isso torna o código muito mais modular e fácil de manter.

Currying

Currying é a técnica de transformar uma função que recebe múltiplos argumentos em uma sequência de funções que recebem um argumento de cada vez. Embora Kotlin não tenha currying nativo, implementá-lo é simples:

fun <A, B, C> curry(f: (A, B) -> C): (A) -> (B) -> C {
    return { a: A -> { b: B -> f(a, b) } }
}

fun somar(a: Int, b: Int): Int = a + b

fun main() {
    val somarCurried = curry(::somar)
    val somar5 = somarCurried(5)

    println(somar5(3))   // 8
    println(somar5(10))  // 15

    // Aplicação parcial diretamente
    val formatarMoeda = { simbolo: String ->
        { valor: Double ->
            "$simbolo %.2f".format(valor)
        }
    }

    val formatarReais = formatarMoeda("R$")
    println(formatarReais(1499.90))  // R$ 1499.90
}

Currying é particularmente útil quando você precisa de aplicação parcial – fixar alguns parâmetros de uma função e gerar uma nova função mais específica. No exemplo acima, somar5 é uma versão especializada de somar que sempre soma 5.

Tail Recursion

Recursão é um pilar da programação funcional, mas recursão clássica pode causar StackOverflowError para entradas grandes. Kotlin resolve isso com a palavra-chave tailrec, que otimiza funções recursivas de cauda convertendo-as em loops internamente:

tailrec fun fatorial(n: Long, acumulador: Long = 1): Long {
    return if (n <= 1) acumulador
    else fatorial(n - 1, n * acumulador)
}

tailrec fun fibonacci(n: Int, a: Long = 0, b: Long = 1): Long {
    return if (n == 0) a
    else fibonacci(n - 1, b, a + b)
}

fun main() {
    println(fatorial(20))      // 2432902008176640000
    println(fibonacci(50))     // 12586269025
}

Para que tailrec funcione, a chamada recursiva precisa ser a última operação da função. O compilador verifica isso e emite um aviso se a anotação for usada incorretamente. Essa otimização é a mesma que linguagens como Scala e Erlang fazem automaticamente.

Avaliação Lazy com Sequences

Quando você trabalha com coleções grandes, as operações map, filter e similares criam coleções intermediárias a cada passo. Sequences resolvem esse problema com avaliação lazy, processando os elementos um a um através de toda a cadeia de operações:

fun main() {
    val resultado = (1..1_000_000)
        .asSequence()
        .filter { it % 3 == 0 }
        .map { it * it }
        .filter { it > 1000 }
        .take(10)
        .toList()

    println(resultado)
    // [1089, 1296, 1521, 1764, 2025, 2304, 2601, 2916, 3249, 3600]
}

Sem asSequence(), cada operação geraria uma lista intermediária de até um milhão de elementos. Com Sequences, os elementos são processados sob demanda e a cadeia para assim que os 10 elementos necessários são encontrados. Se você trabalha com fluxos assíncronos, o conceito de laziness também aparece no Kotlin Flow, que é a contraparte assíncrona das Sequences.

A Biblioteca Arrow

Arrow é a biblioteca de programação funcional mais popular do ecossistema Kotlin. Ela traz estruturas de dados e padrões consagrados do mundo funcional, como Either, Option, Validated e suporte a efeitos computacionais.

import arrow.core.Either
import arrow.core.left
import arrow.core.right
import arrow.core.raise.either

sealed class AppErro {
    data class UsuarioNaoEncontrado(val id: Long) : AppErro()
    data class SemPermissao(val acao: String) : AppErro()
}

fun buscarUsuario(id: Long): Either<AppErro, String> {
    return if (id > 0) "Usuário #$id".right()
    else AppErro.UsuarioNaoEncontrado(id).left()
}

fun verificarPermissao(usuario: String, acao: String): Either<AppErro, String> {
    return if (acao != "deletar") "$usuario pode $acao".right()
    else AppErro.SemPermissao(acao).left()
}

fun main() {
    val resultado = either {
        val usuario = buscarUsuario(42).bind()
        val permissao = verificarPermissao(usuario, "editar").bind()
        permissao
    }

    when (resultado) {
        is Either.Right -> println("Sucesso: ${resultado.value}")
        is Either.Left -> println("Erro: ${resultado.value}")
    }
    // Saída: Sucesso: Usuário #42 pode editar
}

O Either substitui o uso de exceptions para controle de fluxo, que é uma prática considerada inadequada na programação funcional. Com Either.Left representando o erro e Either.Right representando o sucesso, o fluxo de erro se torna explícito e composicional. O bloco either com bind() permite encadear operações que podem falhar de forma sequencial e limpa.

Combinando Tudo na Prática

Vamos juntar vários conceitos em um exemplo mais próximo da realidade – um pipeline de processamento de dados:

data class Pedido(
    val id: Long,
    val valor: Double,
    val status: String,
    val clienteId: Long
)

fun main() {
    val pedidos = listOf(
        Pedido(1, 150.0, "APROVADO", 100),
        Pedido(2, 85.0, "PENDENTE", 101),
        Pedido(3, 320.0, "APROVADO", 100),
        Pedido(4, 45.0, "CANCELADO", 102),
        Pedido(5, 210.0, "APROVADO", 101)
    )

    // Pipeline funcional: filtra, transforma e agrega
    val faturamentoPorCliente = pedidos
        .asSequence()
        .filter { it.status == "APROVADO" }
        .groupBy { it.clienteId }
        .mapValues { (_, pedidosCliente) ->
            pedidosCliente.sumOf { it.valor }
        }

    println(faturamentoPorCliente)
    // {100=470.0, 101=210.0}

    // Funções puras compostas
    val ehAprovado: (Pedido) -> Boolean = { it.status == "APROVADO" }
    val ehValorAlto: (Pedido) -> Boolean = { it.valor > 100.0 }

    val pedidosPremium = pedidos
        .filter { ehAprovado(it) && ehValorAlto(it) }
        .sortedByDescending { it.valor }

    println(pedidosPremium.map { it.id }) // [3, 5, 1]
}

Esse tipo de pipeline é extremamente comum em aplicações reais. Cada função faz uma coisa só, os dados fluem de uma transformação para outra e o resultado final é previsível e testável. Para cenários mais complexos envolvendo processamento assíncrono, você pode migrar essas pipelines para Flow com poucas alterações.

Conclusão

A programação funcional em Kotlin não é uma questão de tudo ou nada. O grande barato da linguagem é justamente permitir que você misture paradigmas conforme a necessidade. Use funções puras e imutabilidade como padrão, recorra a funções de alta ordem para abstrair comportamentos e lance mão de composição e currying quando a complexidade pedir. Com Arrow na jogada, você tem acesso a ferramentas sofisticadas sem precisar abandonar o ecossistema Kotlin que já conhece. Se você quer continuar se aprofundando, confira também o guia de testes em Kotlin para aprender a testar funções puras e pipelines funcionais de forma eficiente. Para quem se interessa por programação funcional em outras linguagens, Rust também oferece suporte robusto a iteradores, closures e imutabilidade por padrão, e Python possui ferramentas funcionais como map, filter e reduce integradas à linguagem.