Kotlin nasceu como uma linguagem multiparadigma, é um dos seus grandes trunfos é o suporte robusto a programação funcional sem abrir mao da orientacao a objetos. Se você vem do Java, vai perceber que Kotlin torna o estilo funcional muito mais acessivel 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 Cidadas de Primeira Classe

Em Kotlin, funções são cidadas de primeira classe. Isso significa que você pode armazenar funções em variaveis, passa-las como argumentos para outras funções e retorna-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"))
    // Saida: Ola, Kotlin Brasil!

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

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

Repare que a variavel saudacao armazena uma lambda diretamente. Nao existe nenhuma cerimonia extra – basta declarar o tipo funcional e atribuir o bloco de código. Isso já e radicalmente diferente do que você encontra no Java, onde seria necessário usar interfaces funcionais ou classes anonimas.

Funções de Alta Ordem

Uma função de alta ordem (higher-order function) e aquela que recebe outras funções como parametro ou retorna uma função. A biblioteca padrão do Kotlin esta 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 e muito útil para construir pipelines de filtragem flexiveis, especialmente quando os criterios vem de fontes diferentes, como inputs do usuário ou configurações dinamicas.

Lambdas e Sintaxe Concisa

Kotlin oferece uma sintaxe de lambda extremamente enxuta. Quando uma lambda tem apenas um parametro, você pode usar it como referência implicita. Alem disso, se a lambda for o ultimo argumento de uma função, ela pode ficar fora dos parenteses.

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 parenteses
val maiusculas3 = nomes.map { nome -> nome.uppercase() }

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

// Referência de funcao
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 e aquela que, dados os mesmos argumentos, sempre retorna o mesmo resultado e não causa efeitos colaterais. Esse conceito esta no coracao 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 previsivel
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 instancias em vez de modificar as existentes. A lista original catálogo nunca e 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 e o ato de combinar funções simples para criar funções mais complexas. Em matematica, a composição de f e g e f(g(x)). Kotlin não tem um operador nativo de composição, mas e 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 transformacao reusaveis. Você define cada etapa isoladamente, testa cada uma separadamente e depois as compoe. Isso torna o código muito mais modular é fácil de manter.

Currying

Currying e 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, implementa-lo e 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 e particularmente útil quando você precisa de aplicação parcial – fixar alguns parametros de uma função e gerar uma nova função mais específica. No exemplo acima, somar5 e uma versão especializada de somar que sempre soma 5.

Tail Recursion

Recursão e 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 ultima operação da função. O compilador verifica isso e emite um aviso se a anotacao for usada incorretamente. Essa otimização e a mesma que linguagens como Scala e Erlang fazem automaticamente.

Avaliacao Lazy com Sequences

Quando você trabalha com coleções grandes, as operações map, filter e similares criam coleções intermediarias a cada passo. Sequences resolvem esse problema com avaliacao 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 intermediaria de até um milhao 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 e a contraparte assíncrona das Sequences.

A Biblioteca Arrow

Arrow e 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) "Usuario #$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}")
    }
    // Saida: Sucesso: Usuario #42 pode editar
}

O Either substitui o uso de exceptions para controle de fluxo, que e 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 explicito 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 proximo 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 e extremamente comum em aplicações reais. Cada função faz uma coisa só, os dados fluem de uma transformacao para outra e o resultado final e previsivel e testavel. Para cenários mais complexos envolvendo processamento assíncrono, você pode migrar essas pipelines para Flow com poucas alteracoes.

Conclusão

A programação funcional em Kotlin não e uma questao de tudo ou nada. O grande barato da linguagem e 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 mao 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.