Desenvolvimento Guiado pelo REPL: Como Eu Realmente Escrevo Clojure
Quando cheguei ao Clojure vindo de Go, "o REPL" soava como o prompt interativo do Python — um rascunho para testar one-liners. Esse enquadramento o subestimava completamente, e levei meses para pegar o fluxo de trabalho de verdade. Desenvolvimento guiado pelo REPL não é digitar num console. É manter um programa vivo rodando e remodelá-lo a partir do seu editor, uma expressão por vez, contra dados reais. Depois que fez sentido, voltar ao editar-salvar-reiniciar pareceu programar de olhos vendados.
O Loop É o Ponto
No meu fluxo com Go o loop é: escrever código, salvar, go run/go test, ler a saída, repetir. O programa está morto entre execuções; toda mudança custa um restart completo. Em Clojure o programa fica vivo. Meu editor está conectado a um processo em execução, e eu avalio a forma sob o cursor — uma única função, uma única expressão — enviando-a para aquele processo vivo e vendo o resultado inline, sem reiniciar nada.
Concretamente: eu escrevo uma função, avalio-a (ela agora está definida no programa em execução), depois avalio uma chamada a ela com um argumento real e leio o resultado logo ao lado do código. Se está errado, edito a função, re-avalio, e chamo de novo — o processo nunca reiniciou, e qualquer estado caro que montei antes (um dataset carregado, uma conexão de banco) ainda está lá.
(defn line-total [item]
(* (:qty item) (:unit-price item)))
;; Evaluate the def above, then evaluate this call right here:
(line-total {:qty 3 :unit-price 250})
;; => 750
Eu não rodei um arquivo de teste nem iniciei um programa. Perguntei ao processo vivo e ele respondeu. Esse feedback é instantâneo e é contra um valor real, que é por que ele parcialmente substitui o compilador do qual sinto falta vindo de Go.
Construindo de Baixo pra Cima, Não Escrevendo de Uma Vez
A mudança mais profunda é que eu não escrevo mais uma função inteira e depois descubro se funciona. Eu a faço crescer. Digamos que estou fazendo parsing e resumindo pedidos. Avalio passos intermediários e mantenho o que funciona:
(def sample (slurp "orders.json")) ; evaluate — now `sample` holds real data
(def parsed (json/read-str sample :key-fn keyword)) ; evaluate — inspect the shape
(->> parsed (filter #(= "paid" (:status %)))) ; evaluate — does the filter work?
(->> parsed (filter #(= "paid" (:status %))) (map :total)) ; add the next step, evaluate
Cada linha é avaliada conforme a adiciono, contra os dados reais, então eu vejo o formato a cada passo em vez de adivinhar e rodar tudo no final. Quando eu dobro isso numa função, já assisti cada estágio funcionar. Isto é o oposto do ritmo do Go, onde escrevo a função inteira contra meu modelo mental dos dados e descubro incompatibilidades só quando rodo.
Rich Comments Guardam o Trabalho de Rascunho
O perigo de toda essa exploração é lixo — chamadas descartáveis espalhadas pelo arquivo. O idioma que resolve isso é o bloco (comment ...), universalmente chamado de "rich comment". Código dentro de comment nunca roda quando o arquivo carrega, mas meu editor felizmente avalia formas individuais dentro dele. Então eu guardo minha exploração ao lado da função que ela exercita, permanentemente:
(defn paid-total [orders]
(->> orders (filter #(= "paid" (:status %))) (map :total) (reduce + 0)))
(comment
;; scratch space — evaluate these by hand, never runs on load
(paid-total parsed)
(paid-total []) ; edge case: empty
(def parsed (json/read-str (slurp "orders.json") :key-fn keyword)))
Seis meses depois esse bloco é documentação: ele mostra à próxima pessoa (geralmente eu) exatamente como dirigir a função com input real.
Enxergando Dentro do Código em Execução com tap>
Para valores enterrados dentro de um pipeline, tap> envia qualquer coisa para um handler registrado sem perturbar o fluxo — como um print, mas estruturado e ligável/desligável:
(add-tap (bound-fn* clojure.pprint/pprint)) ; once, at the REPL
(defn process [orders]
(->> orders
(filter #(= "paid" (:status %)))
(doto tap>) ; peek at the intermediate seq, pass it through
(map :total)
(reduce + 0)))
Diferente de espalhar println, os taps vão para um handler que eu controlo e posso desligar, e me entregam a estrutura de dados real para inspecionar, não uma versão em string.
Onde Morde
Não é de graça. Drift de estado é a armadilha clássica: você redefine uma função, esquece de re-avaliar algo que depende dela, e seu processo em execução não bate mais com seu arquivo. A disciplina é recarregar periodicamente o namespace do disco para que a imagem viva não divirja silenciosamente da fonte da verdade. Recompensa configuração: você precisa da integração REPL do seu editor configurada, e se pular isso e usar um REPL de terminal puro, você tem a ergonomia do prompt-do-Python e nada da mágica. E pode gerar código só-de-REPL — coisas que funcionam no seu processo aquecido mas nunca foram capturadas num teste, então quebram para a próxima pessoa.
A Lição
A mudança de mentalidade em relação a Go é esta: eu costumava tratar o programa em execução como a saída da minha edição. Em Clojure o programa em execução é o meio no qual eu edito. Correção deixa de ser algo que verifico no fim de um ciclo e se torna algo que observo continuamente, forma por forma, contra dados reais. É a metade-fluxo do que faz o Clojure parecer diferente — a metade-linguagem sobre a qual escrevi aqui. Combine-a com o hábito de promover seus experimentos de rich-comment para testes de verdade, e você ganha o feedback rápido sem o drift.