O que Haskell fez com a minha cabeça de programador orientado a objetos
Trocar de linguagem de programação não muda muita coisa na cabeça. De ninguém. Sair de TypeScript para PHP, ou de Java para C#, é o mesmo raciocínio com sintaxe diferente. Você continua pensando em classes, em memória que muda de estado, em coisas que recebem mensagem e alteram algo escondido em algum canto do sistema. Trocar o jeito de pensar é outra história inteira. Foi isso que aconteceu depois de mais de uma década entre Java, JavaScript, TypeScript, PHP, React e Angular, quando resolvi encarar Haskell de verdade: descobri que não estava trocando de ferramenta. Estava desaprendendo um hábito mental que eu nem sabia que carregava.
Mudando a cabeça
O mais difícil de largar não foi a sintaxe. Foi o impulso automático de perguntar “que objeto representa isso?” no lugar de “que transformação leva isso ao resultado que eu preciso?”. Em Haskell não existe algo para consultar e alterar por dentro: existe uma entrada e existe uma regra pura que recebe essa entrada e devolve uma saída, sem guardar segredo em canto nenhum e sem mexer em nada pelas costas de quem chamou. No começo parece uma limitação chata, tipo programar com uma mão amarrada nas costas. Depois de um tempo você percebe uma coisa: essa mão presa era, até onde consigo ver, a fonte de metade dos bugs que você caçava havia anos. Informação escondida dentro de um objeto. Mutação silenciosa num getter que ninguém imaginava capaz de causar efeito colateral. Um comportamento indesejado que ninguém documentou porque ninguém sabia que existia. Pensar em regras e tipos, não em classes e memória compartilhada, força você a declarar tudo o que aquele trecho faz na própria assinatura, sem exceção. Não sei precisar o dia em que isso virou hábito. Só sei que virou, e que não tem mais volta.
Lacunas que o funcional expôs na minha base
O funcional não me ensinou nada do zero, ele só mostrou o tamanho dos buracos que anos de orientação a objetos deixaram: fluência em padrões de design, em herança, em como organizar uma aplicação em camadas, mas quase nenhuma disciplina sobre composição, sobre o que de fato significa uma regra cobrir todos os casos possíveis, sobre como representar “isso pode não existir” sem recorrer a null. Seria fácil demais dizer que eu já desconfiava dessas lacunas havia anos. Não é bem assim, descobri boa parte na base da dor. Quando o compilador começou a recusar meu trabalho porque eu não tinha tratado um construtor do tipo, ou porque uma definição só cobria parte das possibilidades, entendi uma coisa desconfortável: em outras linguagens eu simplesmente não era avisado disso. Descobria em produção. De madrugada. Com um NullPointerException de presente. O sistema de tipos não é burocracia. É um lembrete constante. Chato não, sendo mais justo: humilhante, de vez em quando. A lacuna sempre esteve lá, só que invisível até explodir.
Entendendo a máquina de verdade
Curiosamente, foi a linguagem que a maioria trata como “abstrata demais para o mundo real” que me fez entender melhor o que acontece por dentro da máquina. Avaliação lazy exige raciocinar sobre o momento em que algo é calculado, não só sobre o resultado, e o primeiro thunk empilhado sem necessidade que vira estouro de memória ensina isso na marra (aprendi isso ajustando um parser que devorava memória sem motivo aparente, e ainda tenho raiva daquele bug). Imutabilidade exige pensar em onde uma informação vive e por quanto tempo, em vez de supor que ela está sempre ali, disponível para qualquer parte do sistema mudar sem aviso. Separar efeito de cálculo puro, ao invés de misturar os dois dentro do mesmo método, cobra nomear o que antes ficava implícito: isso lê do disco, isso conversa com a rede, isso é determinístico e testável sem mock nenhum. Programei anos em ambientes orientados a objeto escrevendo trechos que funcionavam sem eu entender por que funcionavam. Aqui não tem essa opção, nem depois de anos de prática. Ou você entende a ordem de avaliação e o ciclo de vida de cada informação, ou o programa não compila. Ou compila e consome toda a memória disponível, só para te lembrar que você não entendeu nada.
Funcional devia ser parte do currículo, não curiosidade de nicho
Vou ser direto porque é a parte que mais me incomoda: programação funcional ainda é tratada como assunto de acadêmico excêntrico ou empresa de nicho, quando devia ser obrigatória em qualquer formação em programação, do mesmo jeito que estrutura de dados é. Por quê? Salvo engano, nenhum professor meu chegou a mencionar isso de passagem. Não porque todo mundo vai escrever Haskell em produção um dia, mas porque o hábito mental que ele exige, pensar em transformação, em composição, em efeitos explícitos, torna qualquer código melhor, inclusive aquele orientado a objetos para o qual você volta depois. Curso que ensina só o modelo imperativo forma profissional que resolve problema sem nunca perguntar se aquele estado mutável tinha razão de existir ali, se aquele null era mesmo necessário, se aquela exception não devia ter sido um valor de retorno desde o início. Não é purismo de linguagem. É o tipo de lacuna que só aparece anos depois, quando alguém como eu precisa desaprender do zero para enxergar o problema que sempre esteve ali.
Hoje não trabalho em nada que não tenha, em algum grau, esse costume de pensar embutido, mesmo quando o resultado final é uma API REST escrita numa linguagem qualquer, orientada a objetos até a raiz. Prefiro composição a herança quando dá. Escolho pureza sempre que o problema deixa. Modelo ausência e erro como valores, não como null e exception. E desconfio de todo estado mutável que eu não consiga justificar numa frase só. Não virei purista: ainda escrevo TypeScript o dia inteiro, e continuo escrevendo. Só que penso diferente enquanto escrevo. Se você nunca sentiu esse desconforto de reaprender a programar do zero, ainda vai sentir, e vai ser bom. Não foi mudança de ferramenta. Foi mudar as regras do jogo. Para melhor, sinceramente.