De Kotlin e Go para Clojure: uma jornada de 8 meses no Nubank.



This content originally appeared on DEV Community and was authored by Fernando Rosa

Olá, pessoal!

Há 8 meses, embarquei na jornada de ser um Lead Software Engineer no Nubank. Vindo de um mundo onde Kotlin e Go eram minhas principais ferramentas, mergulhar em Clojure foi uma mudança de paradigma. Hoje, quero compartilhar um pouco dessa experiência, mostrando com código as diferenças e o que torna Clojure uma linguagem tão fascinante de se trabalhar.

Vamos explorar três problemas simples, resolvidos em cada uma das três linguagens.

Problema 1: O Clássico “Olá, Mundo!”

Tudo começa aqui. Ver a sintaxe mais básica já nos dá uma pista da filosofia de cada linguagem.

Go:
Focado em simplicidade e um ferramental robusto. Tudo é explícito.

package main

import "fmt"

func main() {
    fmt.Println("Olá, Mundo!")
}

Kotlin:
Moderno, conciso e interoperável com Java. A sintaxe é familiar para quem vem do mundo OO.

fun main() {
    println("Olá, Mundo!")
}

Clojure:
Aqui a primeira “estranheza” que vira um encanto. A sintaxe LISP, com parênteses e prefixos (função argumento), trata código como dados. É simples e incrivelmente poderosa.

(println "Olá, Mundo!")

Análise Rápida: De cara, a concisão de Clojure se destaca. A ausência de cerimônias como declaração de package ou main para um script simples já mostra o foco em ir direto ao ponto.

Problema 2: Transformação de Dados – Agrupar e Somar Vendas

Este é um cenário do dia a dia: temos uma lista de vendas e queremos calcular o total por produto. É aqui que a abordagem funcional de Clojure realmente brilha.

Digamos que temos estes dados:
[{"produto": "A", "valor": 10}, {"produto": "B", "valor": 20}, {"produto": "A", "valor": 5}]

Go:
Em Go, faríamos isso de forma imperativa, inicializando um mapa e iterando sobre a lista para acumular os valores. É eficiente, mas verboso.

package main

import "fmt"

type Venda struct {
    Produto string
    Valor   int
}

func main() {
    vendas := []Venda{
        {"A", 10},
        {"B", 20},
        {"A", 5},
    }

    totalPorProduto := make(map[string]int)
    for _, v := range vendas {
        totalPorProduto[v.Produto] += v.Valor
    }

    fmt.Println(totalPorProduto)
    // Output: map[A:15 B:20]
}

Kotlin:
Kotlin oferece uma API de coleções rica e funcional, tornando o código mais expressivo e menos propenso a erros.

data class Venda(val produto: String, val valor: Int)

fun main() {
    val vendas = listOf(
        Venda("A", 10),
        Venda("B", 20),
        Venda("A", 5)
    )

    val totalPorProduto = vendas
        .groupBy { it.produto }
        .mapValues { entry ->
            entry.value.sumOf { it.valor }
        }

    println(totalPorProduto)
    // Output: {A=15, B=20}
}

Clojure:
Em Clojure, a transformação de dados é o coração da linguagem. O código é uma composição de funções, resultando em uma “pipeline” de dados clara e elegante.

(def vendas
  [{:produto "A" :valor 10}
   {:produto "B" :valor 20}
   {:produto "A" :valor 5}])

(def total-por-produto
  (->> vendas
       (group-by :produto)
       (map (fn [[produto lista-vendas]]
              [produto (reduce + (map :valor lista-vendas))]))
       (into {})))

(println total-por-produto)
; Output: {"A" 15, "B" 20}

Análise Rápida: Enquanto Go é explícito e manual, Kotlin e Clojure mostram o poder das abstrações funcionais. A solução em Clojure, com o macro ->> (thread-last), descreve perfeitamente o fluxo: pegue as vendas, agrupe por :produto, depois mapeie cada grupo para calcular a soma e, por fim, transforme tudo em um mapa. É como ler uma receita.

Problema 3: Concorrência – Incrementando um Contador com Segurança

Como lidar com estado compartilhado é um desafio central em sistemas concorrentes. Cada linguagem tem sua abordagem. Vamos simular 1.000 “processos” incrementando um contador.

Go:
Goroutines e Channels são os cidadãos de primeira classe para concorrência em Go. Para estado mutável compartilhado, usamos mutex para garantir a segurança.

package main

import (
    "fmt"
    "sync"
)

func main() {
    var contador int
    var wg sync.WaitGroup
    var mu sync.Mutex

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mu.Lock()
            contador++
            mu.Unlock()
        }()
    }

    wg.Wait()
    fmt.Println("Contador final:", contador)
    // Output: Contador final: 1000
}

Kotlin:
Coroutines são a resposta de Kotlin para concorrência leve. Para estado compartilhado, podemos usar tipos atômicos do Java ou um Mutex específico para coroutines.

import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

val mutex = Mutex()
var contador = 0

fun main() = runBlocking {
    val jobs = List(1000) {
        launch(Dispatchers.Default) {
            mutex.withLock {
                contador++
            }
        }
    }
    jobs.forEach { it.join() }
    println("Contador final: $contador")
    // Output: Contador final: 1000
}

Clojure:
Clojure abraça a imutabilidade e fornece construções simples e poderosas para gerenciar estado quando ele é inevitável. O atom é perfeito para estado compartilhado e não coordenado. A função swap! garante atualizações atômicas.

(def contador (atom 0))

(defn incrementar []
  (swap! contador inc))

(let [processos (repeatedly 1000 #(future (incrementar)))]
  (doseq [p processos] (deref p))) ; Espera todos terminarem

(println "Contador final:" @contador)
; Output: Contador final: 1000

Análise Rápida: As três linguagens resolvem o problema com segurança, mas a abordagem de Clojure é notavelmente mais limpa. Não há locks manuais visíveis no nosso código de negócio. A complexidade da concorrência é abstraída pelo atom e pela função swap!, tornando o código mais simples de ler e escrever.

Conclusão

Trabalhar com Go e Kotlin me deu uma base sólida em sistemas eficientes e bem tipados. Mas a imersão em Clojure no Nubank me ensinou a amar a simplicidade, a imutabilidade e o poder da programação funcional.

A capacidade de moldar o código como uma sequência de transformações de dados e de lidar com concorrência de forma tão elegante não só torna o desenvolvimento mais rápido, mas também mais prazeroso. É uma linguagem que nos convida a pensar no problema de forma diferente e, na minha opinião, de um jeito muito mais direto e poderoso.

E você, já teve uma experiência parecida ao aprender uma nova linguagem que mudou sua forma de pensar? Adoraria saber!


This content originally appeared on DEV Community and was authored by Fernando Rosa