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