Escrito por: Arthur Fücher
Sempre quis contribuir com o Clojure. Não construindo uma nova biblioteca, mas melhorando a própria linguagem. Trabalhar em uma linguagem de programação me parecia difícil demais, para um engenheiro de software sênior.
Durante uma mentoria no Nubank, contei ao Alex Miller que esperava chegar a esse ponto algum dia. A resposta dele foi, mais ou menos: “Você pode fazer isso hoje.”
Nem todo mundo tem a oportunidade de ser mentorado por um contribuidor do core do Clojure. O ponto do Alex era simples: os tickets são públicos, o processo de contribuição é claro e há problemas reais para resolver agora.
A mentoria virou: escolher um ticket aberto, trabalhar nele, aprender. E trabalhei em dois tickets do core do Clojure. CLJ-2897, sobre carregamento dinâmico de bibliotecas via prepl, foi mergeado e incluído no Clojure 1.13.0-alpha5. CLJ-1548, sobre type hints primitivos em protocolos, ainda em revisão quando apresentei esta experiência na Clojure/Conj 2026.
Esperava que o código fosse a parte mais difícil, e foi difícil. Mas não foi a mais difícil: essa acabou sendo quatro perguntas. Perguntas que eu precisava responder antes que alguém sequer olhasse meu código.
1. O que está realmente errado?
Meu primeiro ticket envolvia o prepl do Clojure, um REPL programático. Como um loop read-eval-print convencional, ele aceita expressões e as avalia. Diferente de um REPL convencional, ele emite resultados estruturados. Um desenvolvedor pode se conectar a um processo em execução por meio de um socket e avaliar formas.

Usar add-lib, que adiciona dependências a uma sessão REPL em execução, falhou no prepl:

Essa foi uma mensagem estranha de receber ao usar um REPL. O ticket identificava a causa imediata: o prepl não definia *repl* como true.
Meu primeiro patch adicionou o binding. Foi uma mudança de duas linhas que tratava a causa indicada no ticket. Anexei o patch. Me senti ótimo.
Então o Alex me instigou a considerar o que o usuário queria fazer. Ele não queria um valor específico para *repl*; queria carregar uma biblioteca.
Apliquei o patch e testei: o erro original desapareceu. Mas agora apareceu outra mensagem de erro: Context classloader is not a DynamicClassLoader.
Mesmo usuário. Mesmo objetivo. Outro erro.
add-lib requer um DynamicClassLoader. O prepl não havia configurado um.
Meu patch revisado adicionou tanto o binding de *repl* quanto a configuração do classloader. Com isso, a sessão do prepl passou a conseguir adicionar o Cheshire, fazer require de cheshire.core e chamá-lo.

O primeiro patch eliminou o erro reportado, enquanto o segundo fez a tarefa do usuário funcionar.
Eu até tinha visto, no ticket original, uma referência ao problema relacionado do classloader, CLJ-2540. Li sem perceber o quão central ele era. Ler um ticket e absorver um ticket são coisas diferentes.
Check our job opportunies
2. Por que o comportamento atual existe?
Depois que o patch funcionou, precisei entender por que o prepl não tinha essas peças desde o início.
A explicação era histórica. O prepl foi criado em 2018 copiando e modificando clojure.main/repl. Mas ele não manteve a configuração do DynamicClassLoader do REPL. Mais tarde, em 2023, clojure.main/repl ganhou o binding de *repl*, mas a mudança não chegou ao prepl.
Duas implementações com responsabilidades relacionadas haviam se distanciado. Isso foi consequência de como o código evoluiu. Entender isso levantou uma pergunta maior: devemos corrigir a divergência ou o sintoma dela?
Meu segundo ticket expôs um tipo diferente de descompasso.
No Clojure, temos type hints primitivos. Eles dizem ao compilador para usar uma invocação ciente de primitivos em vez de fazer boxing de um valor. Com funções comuns, isso pode ser uma otimização útil. Mas considere este exemplo de protocolo do CLJ-1548:

Definir o protocolo funciona. Estendê-lo para strings funciona. Chamar a função diretamente falha.
O motivo é sutil. defprotocol aceita o hint primitivo, mas não gera um método especializado em primitivos correspondente. Ainda assim, o hint permanece na metadata de :arglists da função. Quando o compilador lê essa metadata e seleciona uma invocação especializada, neste caso IFn$OLO, uma interface que a função de protocolo gerada não implementa.
O cast da JVM falha antes que o despacho para a implementação de string possa acontecer. A metadata da função promete uma convenção de chamada que a função gerada não consegue cumprir.
O Clojure 1.12 já incluía uma correção para um problema semelhante, sobre hints de retorno primitivos, CLJ-2698. Mas o CLJ-1548 tratava de hints de parâmetros primitivos, e entender a diferença importava.
“Por que isso acontece” não é uma pergunta de curiosidade. É a única forma de decidir se a correção cabe ali.
3. O que já foi considerado?
Ambos os tickets continham mais raciocínio acumulado do que eu percebi no início.
O ticket do prepl apontava para o CLJ-2540 o tempo todo. Um processo de triagem ampliou o ticket do protocolo com o precedente do hint de retorno relacionado e possíveis soluções. Eu não estava chegando a problemas intocados. Outros contribuidores já haviam feito um trabalho investigativo importante.
E, ainda assim, apenas ler essas anotações não foi suficiente. Meu primeiro patch do prepl provou que eu podia ler uma pista e mesmo assim subestimá-la.
Depois que o patch revisado do prepl funcionou, o Alex me incentivou a implementar alternativas. Não apenas pensar nelas. E acabamos comparando três abordagens:
- Copiar o comportamento ausente para o prepl. Adicionar *repl* e a configuração do DynamicClassLoader. O patch era pequeno, cinco linhas adicionadas e quatro removidas, mas não evitaria divergências futuras.
- Reconstruir o prepl em torno de clojure.main/repl. Reutilizar os pontos de extensão do REPL existente, potencialmente reduzindo a divergência. Experimentar isso revelou complicações envolvendo estado compartilhado entre as fases de leitura e avaliação, e o comportamento do prompt. O protótipo teve 65 adições e 44 remoções.
- Extrair a configuração compartilhada do classloader. Introduzir uma macro reutilizável usada pelas duas implementações de REPL. Isso trouxe reutilização limitada, mas ainda não eliminava a divergência.
Os experimentos importaram porque substituíram uma ideia arquitetural atraente por trade-offs concretos. Uma abordagem pode parecer elegante até que precise preservar o tratamento de exceções existente, a estrutura de saída e outros comportamentos que clientes reais esperam.
No total, escrevi 126 linhas de código experimental que não fariam parte da mudança aceita. E quero deixar claro: não foi desperdício. Foi o custo de conhecer diferentes dimensões da solução. Você não sabe se a opção simples está correta até ver quanto as complicadas realmente custam.
4. O que descarta silenciosamente a maioria das opções?
A abordagem que melhor evitava divergências futuras entre prepl e REPL não foi a escolhida.
Reconstruir o prepl sobre clojure.main/repl oferecia a história de reutilização mais clara. Mas acertar o comportamento exigiria modificar o próprio repl. E o repl tem usuários externos que podem depender de detalhes do seu comportamento atual. Alterá-lo para corrigir o prepl imporia risco a código que nunca sofreu o bug original.
Escolhemos o patch menor e menos disruptivo. Ele resolveu o carregamento dinâmico de bibliotecas no prepl sem pretender resolver divergências futuras. Documentamos essa limitação em vez de escondê-la.
O ticket do protocolo tornou a compatibilidade ainda mais concreta. Uma das soluções propostas era rejeitar hints primitivos não suportados no momento da definição.
Para mim, era óbvio: por que deixar desenvolvedores definirem algo que vai falhar quando chamado?
Mas a resposta foi: porque nem toda chamada falha.
Vamos verificar este exemplo usando uma função de protocolo chamada add (a definição é omitida aqui):

Em uma chamada direta, o compilador vê a metadata do hint primitivo e chama .invokePrim. Passar a função como valor usa o IFn.invoke simples, não o invokePrim.
Isso significa que pode haver código com hints primitivos em métodos de protocolo que funciona bem há anos. Rejeitar essas definições quebraria código que roda hoje.
Ou seja, as alternativas tinham consequências diferentes para os usuários existentes.
Nos dois tickets eu tinha mais de uma opção que funcionava. O que foi decidido entre elas tinha menos a ver com o código e muito a ver com tudo ao redor dele.
O desafio final: explicar o raciocínio
A essa altura, eu havia passado um tempo reconstruindo o comportamento e implementando alternativas. Estava pronto para a próxima etapa: o processo de triagem/pré-triagem. Achei que já tinha feito o trabalho difícil.
Então Michael Fogus fez a triagem do ticket do protocolo e deixou seis comentários. Nenhum era sobre o código. Todos diziam respeito à descrição: colocar o problema primeiro, introduzir o contexto na ordem de que um novo leitor precisa e tornar o argumento da mudança mais fácil de seguir.
Reescrevi a explicação do ticket, mas o patch permaneceu o mesmo.
Esse feedback se conectou com algo que Peter Naur escreveu em Programming as Theory Building (1985):
“[…] programar deve ser considerado, propriamente, uma atividade pela qual os programadores formam ou alcançam um certo tipo de insight, uma teoria, sobre os assuntos em questão.”
E ele diz mais uma coisa: a teoria vive nas pessoas. Ela não cabe inteiramente no texto. Então, se a teoria está na minha cabeça e a decisão está na de outra pessoa, como faço para transportá-la?
As quatro perguntas que eu vinha respondendo eram formas de construir uma história. Uma história que move uma teoria de uma cabeça para outra.
A evolução do CLJ-2897 tornou o contraste visível. Sua descrição inicial tinha 82 palavras; a final, 505. Ao lado delas, 9 linhas de código aceito e 126 linhas de trabalho experimental escritas para serem descartadas.
Isso mostra que, para um patch avançar, a história, a teoria, precisava estar na cabeça de quem decide. Um ticket não é burocracia, é a transferência de entendimento.
O que contribuir com o core do Clojure me ensinou
O patch do prepl ficou pronto em agosto de 2025 e chegou ao Clojure 1.13.0-alpha5 em julho de 2026. O ticket do protocolo, aberto em outubro de 2014, ainda estava em andamento quando escrevi isto.
Esse ritmo foi frustrante. E é lento exatamente pela razão que me leva a compartilhar esta experiência. Ali, ninguém aceita código antes de aceitar a teoria.
Entrei nesta experiência preparado para escrever código. O que eu não esperava eram as quatro perguntas. E a quinta coisa: escrever tudo como uma história que constrói a teoria na cabeça de outra pessoa.
Essas atividades foram a contribuição real, junto com o código.
Ainda penso no Alex me dizendo que eu podia trabalhar no Clojure hoje. Ele não estava prometendo um patch rápido nem uma revisão fácil. Estava apontando que eu podia começar a aprender com um problema real imediatamente.
Quer contribuir? Explore as discussões de issues do Clojure, seu tracker de tickets, e o fluxo oficial de contribuição. Escolha um problema e investigue o que as pessoas realmente estão tentando fazer. Leia o histórico. Teste as alternativas. Explique o que você aprender.
Você não precisa de um mentor para perguntar o que o usuário realmente está tentando fazer.
Você pode fazer isso hoje.
Check our job opportunies



Quick Navigation