Deixando Haskell para trás
(journal.infinitenegativeutility.com)- Um programador que usou Haskell como linguagem principal de cerca de 2009 a cerca de 2019 agora já não a considera a opção padrão para projetos pessoais nem para buscar emprego
- O atrativo de Haskell não estava tanto nas mônadas ou nas DSLs em si, mas no raciocínio algébrico sobre código e na refatoração segura e expressão de invariantes que um sistema de tipos forte oferece
- O afastamento ocorreu pelo acúmulo de fatores: uma cultura que favorece recursos avançados de tipos, ferramentas pouco fluidas e mudanças continuamente incompatíveis no GHC e na biblioteca padrão
- Ainda assim, funções de ordem superior, notação
do, lentes, bibliotecas declarativas e abstrações avançadas de tipos continuam sendo vantagens difíceis de outras linguagens substituírem com facilidade - Haskell tem muito valor como linguagem para aprender e se tornar um programador melhor, mas adotá-la como linguagem padrão de uma organização exige aceitar os custos de qualidade das ferramentas e estabilidade da linguagem
A época em que Haskell era a linguagem padrão e a posição atual
- Por cerca de 10 anos, Haskell foi a linguagem padrão para novos projetos
- Depois de conhecer Haskell por volta de 2009, até se mudar por volta de 2019 para um emprego que usava principalmente Ruby e C++, a pessoa se via como programadora Haskell
- Haskell foi usado para vários fins, como ferramentas de linha de comando, serviços web, aplicações gráficas e pequenos scripts
- Hoje, não há uma rejeição completa a Haskell, mas ela já não é a linguagem padrão nem mesmo para projetos pessoais, e também não há mais a busca deliberada por um “emprego com Haskell” como antes
- Esse julgamento não é uma conclusão de que Haskell seja ruim, mas está mais próximo das razões pessoais para ter se afastado de Haskell
Por que Haskell era atraente
-
A propriedade de tratar código algebricamente
- O maior atrativo de Haskell era a possibilidade de raciocinar sobre código de forma simbólica e algébrica
- Por exemplo, quando existe uma função
double n = n + n, a transformação dedouble xemx + xé sempre correta em Haskell - Em muitas linguagens imperativas,
double(i++)e(i++) + (i++)podem se comportar de forma diferente- A primeira incrementa
iuma vez - A segunda incrementa
iduas vezes
- A primeira incrementa
- Graças a essa propriedade, é possível acumular pequenas transformações mecânicas para realizar grandes refatorações com mais segurança
- Uma grande refatoração em uma base de código Ruby pode ser muito inquietante, mas em uma base Haskell pode ser bem mais tranquila
-
Um sistema de tipos rigoroso, mas não excessivamente verboso
- O sistema de tipos de Haskell é rigoroso e, graças à inferência de tipos, não exige tantas anotações de tipo quanto Java
- Typeclasses reduzem o incômodo mesmo em situações em que é preciso se preocupar com tipos
- Um bom código Haskell tem a propriedade de ser “frágil” (brittle)
- Uma abstração incorreta não se dobra silenciosamente para permitir um programa errado
- Se houver um problema, a compilação falha
- O tipo
NonEmptyrepresenta uma lista com pelo menos um elemento- Operações que preservam o tamanho, como
map, retornamNonEmpty - Operações que aumentam o tamanho, como
append, também retornamNonEmpty - Operações cujo resultado pode ficar vazio, como
filter, retornam uma lista comum
- Operações que preservam o tamanho, como
- A uma função que exige que uma lista de caminhos de arquivo seja
NonEmpty, não é possível passar diretamente os argumentos da linha de comando; antes é preciso verificar se ela não está vazia - Se mais tarde for adicionada uma etapa de filtragem por extensão, o resultado pode ficar vazio e deve ser verificado de novo; o compilador impede o caminho em que uma lista vazia seria passada por engano
-
Manter invariantes com tipos
- Ao usar Haskell por muito tempo, torna-se familiar a abordagem de expressar invariantes do programa por meio de tipos e fazer o compilador verificá-los
- Isso não significa apenas cálculos complexos no nível de tipos
- É possível distinguir se uma string SQL foi escapada ou não usando tipos wrapper como
RawStringeSanitizedString- Mesmo que uma API web forneça uma
RawString, a camada de banco de dados pode ser feita para aceitar apenasSanitizedString
- Mesmo que uma API web forneça uma
- Uma representação superficial com muitos campos
Maybepode ser convertida em uma representação interna que garante a existência das informações necessárias - Também é possível tornar algo genérico apenas o suficiente para permitir testes independentes
-
Uma linguagem em que DSLs e funções de ordem superior são naturais
- Haskell facilita a criação de linguagens específicas de domínio concisas mesmo sem macros
- config-ini é um exemplo de biblioteca para lidar com arquivos de configuração INI
- Usa uma interface declarativa em vez de uma interface imperativa de parsing
- Mapeia uma parte específica do tipo de configuração para uma parte específica da estrutura do arquivo INI
- Realiza leitura, escrita e atualização com diff mínimo usando a mesma interface
- DSLs em Ruby podem depender de metaprogramação dinâmica e monkeypatching global, enquanto DSLs em Haskell, com recursos como sintaxe flexível e sobrecarga de mônadas, são mais fáceis de limitar a um arquivo ou região específica do código
- Haskell também torna natural trabalhar com funções de ordem superior
(+)pode ser usado diretamente como uma função que recebe dois argumentos numéricos e os somapairwiseProduct = zipWith (*)expressa de forma muito curta a multiplicação elemento a elemento de duas listas
- A analogia adequada é que Haskell permite lidar com funções do modo como Perl permite lidar com strings
Três fatores que levaram ao afastamento de Haskell
-
Uma atmosfera que favorece estilos difíceis
- O primeiro fator é a preferência estilística por novidades técnicas
- A comunidade Haskell continua experimentando e construindo novas abstrações
- Abordagens vindas da álgebra abstrata ou da teoria das categorias
- Abordagens que elevam mais cálculos ao nível de tipos para restringir estados inválidos
- A API web em nível de tipos do Servant e o caso de representar um sistema de menus com comonad são experimentos de representar domínios de problema de novas maneiras
- Mas, no código do dia a dia, essas abordagens experimentais podem criar mais problemas do que os benefícios inicialmente aparentes
- Em um exemplo de projeto de compilador, o nó da AST tinha o tipo de expressão como parâmetro de tipo
- Se uma árvore sintática com erro de tipo fosse construída, o próprio compilador não compilava
- No início, isso capturou alguns bugs
- Quando muitos passes de otimização não se encaixavam na disciplina de tipos, o código ficou mais complexo com casts e tentativas de apaziguar o GHC
- No fim, os parâmetros de tipo foram removidos da AST
- Em cerca de três quartos dos projetos que tentaram a abordagem de “fancy types”, o valor foi considerado insuficiente
- O movimento Simple Haskell se baseia na ideia de que se obtém o máximo dos pontos fortes da linguagem ao evitar recursos experimentais e permanecer com extensões mínimas
- Recursos de tipos mais complexos estão amplamente disseminados pela comunidade e pelas bibliotecas
- Abstrações de tipos complexas podem dificultar mudanças e manutenção, e houve experiências em que tentativas de transformar de volta abstrações simplificadas em abstrações modernas no nível de tipos levaram a aumento no tamanho do código, piora de desempenho e queda de legibilidade
- Ainda assim, estruturas de dados indexadas por etapa do compilador, como Trees that Grow, continuam sendo exemplos úteis de “fancy type-level feature”
-
Ferramentas que não brilhavam nas tarefas cotidianas
- O segundo fator é um ecossistema de ferramentas desajeitado
- Haskell tem vários sistemas de build e ferramentas de desenvolvimento quase padrão
- Essas ferramentas, em geral, fazem o necessário, mas não chegavam ao nível de serem “adoráveis”
- Houve ferramentas como
ormolu, que fazem seu trabalho com um mínimo de incômodo, mas algumas exigiam bugs constantes e contornos - As ferramentas de Haskell melhoraram ao longo de 10 anos, mas em 2019 não havia nenhuma próxima da usabilidade e estabilidade do Rust Cargo
- A tendência Not-Invented-Here de continuar criando algo novo e melhor também era um problema
- Há até um movimento para retrabalhar o formato textual de pacotes com hpack
- É frustrante quando esse tipo de tentativa permanece 95% concluída por anos
- A avaliação não é que as ferramentas de Haskell sejam péssimas, mas “razoavelmente boas” não satisfaz um padrão que ficou mais alto
-
Mudanças incompatíveis contínuas
- O terceiro fator são as mudanças contínuas pelas quais a linguagem Haskell e o GHC passam
- O GHC é tratado como, na prática, a única implementação pública razoável de Haskell
- Haskell tenta corrigir gradualmente erros e inconsistências do passado mesmo na biblioteca padrão e nas partes básicas da linguagem
- Essas mudanças podem levar a linguagem a fundamentos mais limpos e consistentes, mas criam atritos pequenos e contínuos para os usuários
- As mudanças Foldable-Traversable no Prelude, de 2015, são um exemplo desse tipo de mudança
- Projetos Haskell antigos podem não compilar no GHC mais recente mesmo sem dependências externas
- adição de superclass
- remoção de import
- adição de assinaturas de tipo por causa de polimorfismo que se tornou ambíguo
- As correções costumam ser pequenas, mas se acumulam, e as partes que mudaram um pouco em relação ao passado exigem a atenção do usuário
- O método de atualização por epochs do Rust serve de contraste ao explicitar um contrato de compatibilidade retroativa para recursos estáveis e esconder breaking changes intencionais atrás de uma epoch
Pontos fortes de Haskell que ainda fazem falta
- A escrita e refatoração algébrica de código em Haskell continuam sendo pontos fortes
- Rust também compartilha muitas vantagens com Haskell, mas a confiança de que pequenas refatorações mecânicas preservam o significado do programa não é tão fácil quanto em Haskell
- O sistema de tipos também oferece recursos que outras linguagens geralmente não têm
- As linguagens mainstream de 2023 avançaram muito em recursos de tipagem estática em comparação com 2009
- Ainda assim, recursos como abstrações sobre higher-kinded types continuam sendo diferenciais de Haskell
- O ecossistema de bibliotecas Haskell é misto
- Bibliotecas open source mantidas pela metade
- Pequenos reinos em que autores individuais tentam reconstruir o mundo à sua maneira
- Tipos semelhantes a string que são necessários, mas diferentes entre si
- Ainda assim, Haskell tem excelentes bibliotecas e abstrações que em outras linguagens só são possíveis pela metade
- Lentes são uma abstração poderosa cujas vantagens só aparecem bem depois de bastante uso
- Brick oferece uma biblioteca de UI de terminal muito boa com blocos declarativos
- diagrams e music-suite também são bons exemplos de APIs declarativas
- A notação
dotambém é um recurso de que se sente muita falta- O ponto central não é a
doem si, mas uma notação em que adicionar bindings não aumenta continuamente o nível de indentação - O
flat_mapaninhado de Ruby aprofunda a indentação a cada novo eixo - A notação
dode Haskell expressa a mesma combinação de forma plana
- O ponto central não é a
- A notação
dopermite embutir concorrência estruturada no estiloasync/await, transações concorrentes e programas probabilísticos com a mesma sintaxe leve
Deve-se aprender Haskell? Deve-se usá-la?
- A resposta para “devo aprender esta linguagem, devo usá-la” depende do objetivo
- Se o objetivo é se tornar um programador melhor, Haskell vale muito a pena aprender
- Haskell é uma linguagem interessante e poderosa
- A experiência de aprender Haskell é avaliada como algo que aumentou a habilidade de programação
- Se o objetivo é uso prático, é preciso ter mais cautela
- As vantagens de Haskell existem de fato
- É preciso compará-las com o quanto uma pessoa ou organização consegue tolerar os atritos mencionados acima
- Também existem pessoas e organizações que obtêm benefícios suficientes em Haskell para manter os pequenos incômodos no nível de pequenos arranhões
- Se a exigência for um sim ou não claro, a resposta fica mais próxima de “não use Haskell”
- O julgamento é que, ao criar uma organização de engenharia, Haskell não seria estabelecida como linguagem padrão
- Isso porque tanto o valor de boas ferramentas quanto o custo de ferramentas ruins são conhecidos
- A preferência é por uma linguagem que possa oferecer imediatamente uma linguagem estável, um ecossistema de código claro e ferramentas excelentes
- Ainda assim, Haskell oferece ferramentas excelentes para segurança e correção de programas
- Em certos contextos, pode valer a pena aceitar ferramentas ruins e linting para impedir type families desnecessárias
- Haskell já não é uma linguagem adequada pessoalmente, mas a conclusão é que, se Haskell não existisse no mundo, o mundo seria pior
1 comentários
Opiniões no Hacker News
Usei Haskell por uns 10 anos e recentemente deixei de usá-lo como meio de sustento, e concordo claramente com o primeiro ponto do autor: a comunidade Haskell valoriza muito o aprendizado
É bom trabalhar com pessoas curiosas e ter sempre algo a ensinar e aprender, mas a comunidade é fraca em descartar ideias que foram tentadas; por isso, em bases de código Haskell profissionais, se não houver uma gestão rigorosa, é fácil acumular muita coisa do tipo “dá para fazer, mas é melhor não usar”
Dito isso, discordo quanto à toolchain. É verdade que as ferramentas de Haskell não são ótimas, mas, depois de usar Python, JS, Java, Rust e Elm, achei que a maioria das ferramentas também era ruim, e cheguei até a sentir falta da toolchain de Haskell. O cargo é excelente, mas é quase uma exceção, e fico curioso para saber se Rust conseguirá evitar esse estado depois de envelhecer
Tenho um projeto em que trabalho de vez em quando, e tenho 100% de certeza de que, quando eu o abrir da próxima vez, ele não vai rodar de novo. O gerenciamento de dependências e ambientes virtuais do Python é igualmente ruim, mas o Poetry foi bem razoável; em Ruby, seja usando chruby/rbenv/rvm, tudo funciona de forma parecida e bem, e o Bundler também roda bem. Java tem seus defeitos, mas a experiência com IDE é de altíssimo nível e, embora a refatoração não seja tão segura quanto em Haskell, é muito fácil de fazer
O Bundler em geral não é ruim e, mesmo que eu discorde de algumas escolhas de design, se você seguir o caminho correto, muita coisa se encaixa bem. Ferramentas de linguagem também podem ser feitas de uma forma suficientemente menos quebrada e, embora eu não tenha usado cargo diretamente por muito tempo, se ele realmente é o melhor da categoria, não seria surpreendente que mantivesse esse estado
Em termos de ferramentas, pelo que sei, Haskell tem uma coisa com a qual outras linguagens dificilmente competem: o Hoogle
O Hoogle é incrivelmente bom. Você diz em Haskell o que quer, e ele diz o que é possível fazer em Haskell. Houve tentativas parecidas em Rust, e também tentei criar um Noogle para Nim, mas em linguagens em que há uma separação clara entre “passar argumentos para uma função” e “chamar uma função”, isso não funciona do mesmo jeito
Sempre que vejo código Rust envolvendo
ResulteBox, queria que existisse um Roogle tão útil quanto o Hoogle. Fora isso, as ferramentas de Haskell não têm vantagens. Em boa parte, isso parece se dever ao instinto da comunidade de criar algo “inovador” em vez de melhorar o que já existeNão é perfeito, mas está muito melhor do que há 5 anos
Gostaria que mais projetos seguissem essa direção
Acho que o maior problema é que o GHC fica preso a uma versão específica de
base, a biblioteca padrãoQuando
basemuda e uma nova versão do GHC só dá suporte a essa versão, você acaba tendo que trocar praticamente todas as dependências para usar um GHC mais novo. Ainda não entendo por que código compilado com GHC 9.6 precisa usarbase4.18.0.0. Por que o binário do GHC deveria se importar com a versão do móduloData.Listdo código que está compilando?Entendo que elementos específicos do GHC expostos por
basefiquem vinculados a uma versão específica do GHC, mas não sei por que o restante também precisa ser assim. Pelo que entendo, porém, há um trabalho em andamento para corrigir isso dividindobaseem vários pacotes: https://gitlab.haskell.org/ghc/ghc-wiki-mirror/-/blob/master...basepor considerá-lo inválidobasetrocarem instâncias deData.List, surgem problemasPor exemplo, se você tiver uma lista de
Data.List, seria possível concatenar nela os resultados de funções de duas bibliotecas diferentes?basereinstalável é uma meta que todos querem. A situação atual se deve em grande parte a razões históricas e ao fato de elementos primitivos vinculados ao interior do compilador estarem acoplados a elementos não primitivos; com esforço suficiente, isso pode melhorarSe houve três grandes motivos para meu interesse por Haskell esfriar aos poucos, foram a preferência estilística pelo novo, que exalta código obscuro e dificulta a manutenção; ferramentas estranhas que tornam o trabalho cotidiano mais lento; e mudanças contínuas que, embora ocasionais, exigem atenção constante e quebram coisas regularmente
É uma crítica válida. Aprendi bastante Haskell entre 2010 e 2012, e a linguagem em si era excelente, mas a documentação e as ferramentas eram difíceis de lidar. A comunidade foi do Cabal e do infame Cabal hell para o Stack, depois voltou ao Cabal, e no geral as coisas melhoraram
Enquanto isso, outras linguagens de programação também absorveram elementos de programação funcional. Por exemplo, Java adicionou Streams, funções, lambdas, tipos algébricos de dados, records e pattern matching; mesmo que a sintaxe não seja tão elegante quanto a de Haskell, os conceitos básicos de programação funcional estão lá
Assim, criaram para si um poço de sofrimento sem fim, que só se tornou minimamente suportável porque os raios de luz não baixam o bastante para que se vejam as cores que a luz cria nas árvores do lado de fora. Aqueles vindos de fora, divididos entre um dialeto ajustado à escuridão que eles mesmos criaram e outro dialeto que floresce na luz, mas ali sufoca, rapidamente perdem a sanidade
Se a comunidade voltou ao Cabal, fico curioso para saber o que perdi
Por esses motivos, migrei para F# e praticamente não olhei para trás. Às vezes sinto falta de higher-kinded types, mas F# também tem generics e, sendo sincero, ele meio que me força a escrever código mais simples do que em Haskell, e em geral o resultado é melhor
Dito isso, F# não dá aquela sensação de “programador avançado” como Haskell. É parecido com Haskell, mas totalmente pragmático. Faço muita coisa em F# e gosto de usá-lo, mas às vezes olho para trás com certa nostalgia e me pergunto por que não deu certo com Haskell
Muitas vezes me perguntei se eu não estaria perdendo algo por não usar Haskell, mas para meus objetivos provavelmente não
Aprender Haskell parece tentar decifrar uma língua alienígena. Se você está acostumado a laços if-else comuns e atribuições simples de variáveis, prepare-se para dar um nó na cabeça
monad não é um monstro cósmico, mas é uma abstração estranha que pode fazer você coçar a cabeça e questionar suas escolhas de vida. As bibliotecas também dão a sensação de revirar um brechó meio vazio para encontrar o que precisa, e metade da documentação parece ser lida como hieróglifos
Em termos de desempenho, na prática você pode passar um bom tempo tentando entender por que a avaliação preguiçosa faz o programa consumir mais memória do que o esperado e ficar mais lento. Empregos também são difíceis de encontrar, a menos que seja uma empresa de nicho muito específico totalmente comprometida com Haskell, e depurar parece procurar uma agulha em uma pilha de hieróglifos funcionais
Tenho usado Scala e acho que ela consegue oferecer o melhor dos dois mundos. Dá para raciocinar de forma algébrica e, se o código compila depois de uma refatoração, em geral funciona
A inferência de tipos e monads também funcionam bem, e você ainda ganha os benefícios do ecossistema Java. Quando as coisas ficam obscuras demais, dá para quebrar as regras e codar de um jeito mais próximo de Java. Tenho curiosidade para saber o que os outros pensam
Mas o sbt é de enlouquecer, e provavelmente é a parte de que menos gosto na experiência de desenvolvimento
Os órgãos de governança e mantenedores de Haskell realmente parecem querer que a linguagem seja favorável a pesquisa, experimentação e exploração acadêmica
Isso por si só é bom, mas em algum ponto inevitavelmente entra em conflito com os interesses de programadores que querem usar Haskell como uma ferramenta prática e estável. O que parece necessário é a capacidade de marcar coisas experimentais e de ponta em uma trilha separada. Seja por pragma, seja por declaração em nível de pacote, deveria ser possível olhar para um pacote e saber se vale trazê-lo para dentro
Assim, desenvolvedores voltados à prática poderiam adotar políticas como “usamos apenas stable Haskell”. Reconhecendo a dinâmica de “Avoid success at all costs”, em que uma adoção alta demais de Haskell prenderia a linguagem na inércia e a impediria de impulsionar conceitos de ponta, ainda assim é preciso uma trilha oficial para que desenvolvedores do dia a dia possam permanecer em um subconjunto mais restrito e estável de Haskell
Java nunca terá algo assim. https://www.stackage.org/
No fim, simplicidade depende de quem vê. Mesmo recursos de tipos sofisticados, quando você internaliza como funcionam, têm usos que tornam os tipos mais expressivos, deixam o código mais seguro e até mais simples
É um texto bem bom. Mas o ponto fraco é que nunca senti pressão para usar algum recurso de tipos novo e chamativo específico se eu não quisesse
Se você não gosta da API HTTP em nível de tipos do Servant, pode descer um nível e usar o warp. As bibliotecas entendem que tipos excessivamente complexos podem virar uma armadilha, então muitas vezes são organizadas em camadas desse jeito. É preciso ter intuição sobre até onde ir, e esse processo inclui erros
Li a documentação do Servant, mas, como era de se esperar, ela pressupunha mais entendimento de Haskell do que eu tenho
Esse texto realmente bateu comigo. Uso Haskell há cerca de 1,5 ano em uma aplicação bastante industrial, e antes disso também tive bastante experiência com coisas como https://github.com/mattgreen/hython
Em 20 anos de carreira, fazia tempo que eu não precisava aprender tanta coisa, e isso é resultado da combinação entre o domínio e a linguagem. É empolgante, mas também cansativo, e pode ser um bloco grande demais para lidar junto com uma vida familiar corrida
Ainda assim, a linguagem é excelente. Meu gerente em geral recomenda usá-la de forma top-down, no sentido de pensar profundamente em como o código deve ser lido e começar por aí, porque a flexibilidade do Haskell comunica bem a intenção do desenvolvimento. É uma grande mudança de mentalidade em relação a ficar digitando
functionrepetidamente na maioria das linguagens enquanto o cérebro entra em modo de economia de energia, e ela realmente parece projetada para favorecer a escritaMas, mesmo tratando efeitos controlados com monads, a sintaxe ainda é difícil de ler por alto. Talvez seja porque uma linha pode ficar densa demais. Sinto muita falta de retornos antecipados, e também do excelente LSP do Rust. O Haskell perdeu recentemente até o recurso de preencher pattern matches por causa de mudanças internas na série 9.x, e empilhar monads ainda é algo de que não gosto e em que erro com frequência. Os tempos de compilação podem ser brutais, especialmente quando se encontra a biblioteca lens
Também não gosto da avaliação preguiçosa como padrão. A comunidade, em certa medida, já reconhece que programas em Haskell ficam muito mais vulneráveis a vazamentos de espaço por causa desse padrão, e que isso pode passar despercebido por bastante tempo em muitos programas. Como programador de sistemas, isso me faz querer gritar
Mesmo assim, a comunidade é um dos grupos de programadores mais fortes que já vi. Não quero ficar repetindo o meme de que são inteligentes; o ponto é que eles realmente pensam profundamente e empurram as coisas para a frente. No nosso domínio, análise de programas, Haskell encaixa como uma luva, mas fora disso ainda fico um pouco cauteloso. No fundo, ainda tenho sede de uma linguagem de programação que seja OCaml, mas com sintaxe melhor
Nem é preciso expor ExceptT na interface; dá para usá-lo internamente apenas para retorno antecipado e fazer a função retornar Either
É uma pena que no fim ela não tenha ganhado muita tração
A integração entre orientação a objetos e programação funcional é melhor que a do OCaml, e o código idiomático em OCaml é parecido o bastante para ser fácil de portar com uma transformação simples