Tipos estáticos e pás
(carefully.understood.systems)- A razão pela qual a popularidade dos tipos estáticos caiu dos anos 2000 até o início dos anos 2010 e voltou a crescer de meados para o fim dos anos 2010 é explicada pela melhora na qualidade dos sistemas de tipos estáticos
- Sistemas de tipos dinâmicos exigem que a pessoa julgue diretamente o estado e o conteúdo de variáveis e campos, sendo comparados a cavar a terra com as próprias mãos, já que o computador nem ajuda nem atrapalha
- Sistemas de tipos estáticos antigos, como os do Java inicial ou do C++98, não ajudavam nem mesmo a distinguir ponteiros nullable de non-nullable e obrigavam a repetir nomes de tipos, sendo comparados a uma pá de papel
- Sistemas de tipos modernos como TypeScript, Haskell, MyPy, Swift e Rust oferecem melhor suporte para verificar erros de programa e representar estados por meio de tratamento de null, tipos soma e union types, além de inferência de tipos
- Com a popularização de recursos de IDE como autocompletar nomes de métodos, as informações inseridas no sistema de tipos estáticos passaram a gerar, além da verificação de erros, ganhos de produtividade
Argumento central
- A popularidade dos tipos estáticos não é apenas uma questão de moda; ela voltou a crescer porque a qualidade dos sistemas de tipos estáticos amplamente utilizáveis melhorou
- Usa-se a analogia de que, se houver uma boa pá, é melhor cavar com uma pá do que com as mãos, mas se só houver uma pá feita de papel, é melhor usar as mãos
- Em sistemas de tipos dinâmicos, a pessoa precisa pensar diretamente sobre quais estados e conteúdos as variáveis e campos de um programa possuem, e o computador não ajuda nesse julgamento
- Um sistema de tipos estáticos ruim pode trazer mais peso do que ajuda, o que é comparado à situação de cavar a terra com uma pá de papel
Limitações dos sistemas de tipos estáticos do passado
- Os sistemas de tipos estáticos do Java inicial e do C++98, amplamente usados nos anos 90 e no começo dos anos 2000, não ajudavam direito nem na tarefa simples de distinguir ponteiros nullable de ponteiros non-nullable
- Os sistemas de tipos estáticos do passado são descritos como estruturas sem tipos soma, tendo apenas tipos produto
- Os sistemas de tipos estáticos do passado faziam a pessoa escrever manualmente nomes de tipos em vários lugares
- Um código como
BufferedReader bufferedReader = new BufferedReader(new FileReader(filename));é descrito como um pequeno desastre
Melhorias dos sistemas de tipos modernos
- Sistemas de tipos modernos como TypeScript, Haskell, MyPy, Swift e Rust oferecem formas de distinguir tipos nullable de tipos non-nullable
- São dados como exemplo
Maybe tno Haskell,T | nullno TypeScript,T?no Swift eOptional<T>no Rust, mostrando que o sistema de tipos pode indicar onde a verificação de null é necessária e onde ela foi omitida - Explica-se que, na prática, isso faz com que erros de null pointer em runtime quase deixem de aparecer
- Sistemas de tipos modernos oferecem pelo menos tipos soma ou union types, possibilitando colocar em prática "Make invalid states unrepresentable"
- Essa abordagem permite representar, em objetos que modelam máquinas de estado, que cada campo exista apenas quando estiver relacionado ao estado correspondente
- Sistemas de tipos modernos oferecem inferência de tipos, então, se o compilador consegue entender
let x = 5;como um número, não é necessário escreverlet x: number = 5;
Recursos de IDE e conclusão
- Com a disseminação de recursos de IDE como autocompletar nomes de métodos, a utilidade dos sistemas de tipos estáticos aumentou ainda mais
- Nos anos 90, o Intellisense era um recurso central do Visual Studio, mas nos anos 2020 funcionalidades semelhantes estão presentes em quase toda IDE e editor
- As informações colocadas em um sistema de tipos estáticos geram não só verificação de erros do programa, mas também ganhos adicionais de produtividade
- Um bom sistema de tipos dinâmicos é melhor do que um sistema de tipos estáticos ruim, mas hoje é possível usar sistemas de tipos estáticos muito melhores do que os do passado
1 comentários
Opiniões no Lobste.rs
O texto é bom, mas não concordo completamente. Mesmo que os sistemas de tipos estáticos do começo dos anos 2000 não fossem excelentes, ainda eram muito melhores do que não ter tipagem estática nenhuma
Não havia tipos soma fechados, mas muita coisa podia ser modelada com subtipos; não havia tipos não anuláveis, mas as referências e tipos sem ponteiro de C++, além dos tipos primitivos de Java, cobriam parte disso. Em Ruby ou JavaScript, todos os tipos não só podiam ser nulos, como também podiam ser tratados como string, inteiro ou qualquer outro tipo do programa, o que era uma situação pior
Acho que uma grande razão para a mudança de rumo em relação à tipagem estática foi que, no boom das redes sociais da Web 2.0, a vantagem de sair na frente era mais importante do que qualquer outra coisa. Mesmo acumulando dívida técnica com Ruby ou Python, valia mais a pena lançar rápido e iterar do que ser deixado para trás como o Friendster ou o Digg, e, se estivesse lento, dava para simplesmente comprar mais servidores com o capital barato de juros baixos que era fácil conseguir naquela época
Depois, no boom mobile, o software passou a rodar em dispositivos de usuários limitados e fora de controle, e aplicativos lentos com tipagem dinâmica eram simplesmente lentos na prática, enquanto erros de tipo também não podiam ser recuperados elegantemente com um handler de resposta no topo, como em um servidor. Nesse ambiente, a segurança e o desempenho da tipagem estática ficaram muito mais convincentes
No começo dos anos 2000 eu também concordava, porque os sistemas de tipos da época geralmente só forçavam propriedades que quase nunca estavam erradas, enquanto impunham restrições que não ajudavam a estruturar o código. Em especial, a forma como subtipagem e herança de implementação vinham acopladas não era flexível
Minha visão mudou ao usar sistemas de tipos mais modernos. No snmalloc, o sistema de tipos de C++ impõe uma máquina de estados de posse de memória, e em outros codebases verifica o comportamento correto de overflow em contadores de ring buffer. Nos dois casos, se desse errado, seria chato de depurar e uma fonte comum de erro, mas o compilador realmente falhou em código que eu achava que estava certo, impedindo que o bug entrasse na árvore
No IDE, apertar
.e digitar um pouco do nome do método até dar Enter na sugestão certa economiza 2 segundos a cada poucos segundos, e também economiza aqueles 30 segundos de ter de procurar a definição da classe quando você não sabe quais métodos existem. Esse princípio também aparece muito bem em https://grugbrain.dev/#grug-on-type-systemsComo escrevemos linhas que chamam métodos com muito mais frequência do que escrevemos os tipos dos parâmetros das funções, a troca pesa de forma esmagadora contra a tipagem dinâmica. O que realmente tinha valor não era permitir código absurdo que só vai falhar em runtime, e sim omitir o tipo de variáveis locais, e linguagens estáticas nunca precisaram proibir isso em primeiro lugar
Nos raros codebases que usavam o sistema de tipos a sério, acumulavam-se páginas e páginas de código que não diziam nada, e ainda assim havia montanhas de condicionais em runtime; além disso, em Java, à medida que a hierarquia de tipos crescia, o programa também ficava efetivamente mais lento. A maioria dos codebases usava tipos de forma esparsa e colocava muitas condicionais em runtime, sem grande economia na cobertura de testes necessária em comparação com um sistema de tipagem dinâmica
Linguagens dinâmicas não ofereciam recompensas estáticas, mas eram concisas, fáceis de ler e revisar, e também fáceis de testar. Isso era especialmente verdade em ambientes como os frameworks de injeção de dependência do fim dos anos 90 e começo dos 2000, em que adicionar um novo serviço exigia editar vários arquivos XML. Também dava para trabalhar sem um IDE consumindo metade da RAM
Como meu início de carreira foi exatamente assim, concordo bastante com o texto. A relação custo-benefício de Java 1.4 até Java 6 era tão ruim que quase me fez desistir de linguagens estaticamente tipadas, e só anos depois, mexendo com Haskell por hobby, percebi que tipagem estática podia sim ter uma relação custo-benefício razoável e que o problema era o Java. O ensaio “python is not java” também mostra bem aquele período sombrio
Tenho dúvidas se realmente vimos, na prática, o ganho de confiabilidade do nosso software depois que tipagem estática virou o espírito do tempo
Sempre achei que as vantagens da tipagem estática estavam muito mais no feedback imediato durante o desenvolvimento e na redução de falhas fatais em runtime; em teoria essas falhas sempre podem acontecer, mas na prática não parecia que ocorriam com tanta frequência
undefinedenullcaíram drasticamenteAlguns juniores e até alguns seniores eram céticos no começo e achavam que isso só ia espalhar
@ts-ignorepor todo lado, mas na prática surgiram só uns três casos, incluindo os causados por tipos quebrados em dependências. Antes, o app quebrava mais ou menos uma vez por semana na branch de desenvolvimento por confusão de tipos e travava meu trabalho; hoje nem lembro quando foi a última vez que isso aconteceuSó de satisfazer o
tsc, já diminuem os bugs relacionados a tipos, inclusive quando eu mesmo não escrevi o código. Em contrapartida, os linters hoje em dia são zelosos demais, e ao tentar satisfazer ferramentas como o Sonar já vi refatorações quebrarem de verdade. 95% dos avisos eram falsos, 3% eram bugs da própria ferramenta, e os 2% que ajudaram também não eram a causa real de bugs. Em vez de gastar uma semana ajustando a base de código para pegar um bug, acabei introduzindo mais dois no processoO trabalho de satisfazer o
tscgerava mais ou menos 2 correções de bug reais por dia e 1 regressão, mas a regressão em geral tinha gravidade menor, mais como comportamento incorreto do que um crash completoSe você adicionar testes baseados em propriedades a isso, levava em média de 2 a 4 horas e sempre revelava pelo menos um bug. Se o código permite testes baseados em propriedades, você deveria fazer isso
Usando o modelo barato DeepSeek V4 Flash para ampliar a cobertura de testes, tomando cuidado para não criar testes lixo, eu corrigia uns 2 ou 3 bugs lógicos por dia, sem crashes. Mas o conjunto de testes mal é sustentável em termos de manutenção
Quando deixei um júnior criar testes de qualquer jeito com modelos da linha Sonnet e Opus 4.5 e 4.6, os modelos só produziam testes que “documentavam o comportamento atual”, então o efeito das correções era pequeno, e o conjunto de testes ficou impossível de manter, então tivemos de descartá-lo
Testes baseados em modelos são muito bons para encontrar bugs, mas a configuração é complexa, e é bem trabalhoso induzi-los a explorar os cantos do sistema sem desperdiçar ciclos em funcionalidade superficial. Algo como um fuzzer baseado em modelos com perfil parece interessante
Resumindo: verificadores de tipo são bons em capturar falhas fatais e vários tipos de confusão, e testes baseados em propriedades são excelentes. Testes comuns exigem muita disciplina para gerar retorno consistente
O ponto com que mais discordo aqui é juntar TypeScript com um bom sistema de tipos
awaitjá me pegou várias vezes. Ainda assim, é fato que melhorou a situação de forma dramáticaSinceramente, até tipagem estrutural eu acabei aceitando, e acho que isso vai influenciar positivamente o design de linguagens no futuro
Esse argumento é pouco convincente. Linguagens de programação decentes com tipos algébricos de dados e inferência de tipos já existiam desde meados dos anos 90
Os sistemas de tipos de Java e C++ eram muito pobres, mas SML, OCaml e Haskell já existiam e a sensação era bem parecida com a de hoje. Se as pessoas não usavam essas linguagens, isso é uma questão de cultura, adoção e requisitos não verbalizados, e não algo que se explique apenas por “os sistemas de tipos utilizáveis não eram bons o bastante”
Ou, se a tese for “os sistemas de tipos das linguagens populares da época eram ruins, e os das linguagens populares de hoje são melhores, então sistemas de tipos ficaram mais populares”, isso soa como raciocínio circular
Também há muita nuance na diferença entre linguagens projetadas junto com um sistema de tipos e linguagens originalmente projetadas sem tipos, às quais um sistema de tipos foi acoplado depois
Mesmo vindo de uma posição que historicamente preferia tipagem dinâmica, acho este texto bastante justo. Hoje trabalho com C#, uso Lisp por hobby e antes também usei Python
Quando eu tinha de usar Java 5, em geral ficava brigando com o sistema de tipos por causa de decisões ruins dos autores de bibliotecas. Depois que migrei para C# por volta de 2010, o sistema de tipos deixou de ser ativamente prejudicial, mas no geral era redundante e ainda não evitava a confusão de tipos mais comum em Python, a exceção de ponteiro nulo
O sistema de tipos de C# só começou a me ajudar de verdade por volta de 2020, quando entraram os tipos de referência não anuláveis. Este ano também entram tipos união nativos, mas bibliotecas de tipos união que forçam exaustividade já eram possíveis pelo menos desde 2016, e eu comecei a usá-las em 2020
Ainda acho que moda continua tendo um papel, mas parte disso não é ruim. Linguagens da moda com sistemas de tipos mais expressivos também trouxeram melhorias para as linguagens comuns que usamos para ganhar dinheiro
Haskell e seu sistema de tipos já existiam nos anos 2000. Não eram tão amplamente usados quanto hoje, mas certamente existiam, então essa afirmação precisa ser ajustada nesse ponto.
Pessoalmente, acho que o TypeScript foi um grande fator para familiarizar usuários de linguagens mainstream com sistemas de tipos melhores. Além da qualidade e do apoio da Microsoft, ele tinha a vantagem de se aplicar ao JavaScript, e o JavaScript precisava de tipos mais urgentemente do que o Python. Por causa de “Undefined is not a function.” e de “The good parts.”
“Real World Haskell” saiu em 2008 e tinha como objetivo fazer o Haskell parecer mais atraente para programadores mainstream. Não sei o quanto isso ajudou a espalhar a boa nova.
No mundo Java, o Scala trouxe tipos interessantes em 2004, e no .NET o F# apareceu em 2005. O Scala talvez tenha conseguido os usuários de destaque mais visíveis, como o Twitter, mas não estava numa posição de absorver uma grande parcela dos usuários daquela plataforma como o TypeScript, nem era atraente o bastante para atrair em massa usuários de outras linguagens como Rust ou Go
No parágrafo seguinte, ele menciona Haskell como um “sistema de tipos moderno”, mas a porcentagem de pessoas com experiência em Haskell no fim dos anos 90 e começo dos anos 2000, incluindo quem só mexeu por conta própria, era na prática próxima de 0%. O texto está falando de como a maioria dos desenvolvedores daquela época experimentava linguagens com tipagem estática e por que essa maioria evitava coletivamente linguagens desse tipo
Por exemplo, para usar
duneem OCaml, você precisa entender arquivosopam, arquivosdune, a sintaxe deocaml modulee a sintaxe deocaml. As extensões opcionais de compilador do Haskell passam a mesma sensação intimidadoraEm contraste com o
cargo, em que basta conhecertomle Rust