Tipagem estática e pás
(carefully.understood.systems)- A razão pela qual a popularidade da tipagem estática caiu dos anos 2000 até o início dos anos 2010 e voltou a subir em meados/fim dos anos 2010 foi a melhora na qualidade dos sistemas de tipos estáticos, não uma moda
- Ao cavar um buraco, assim como é melhor usar as mãos do que uma pá de papel, um sistema de tipos dinâmico é melhor do que um sistema de tipos estáticos ruim
- Sistemas de tipos estáticos antigos, como o Java inicial ou o C++98, nem sequer distinguiam nullable de non-nullable, não tinham tipos soma e exigiam escrever nomes de tipos manualmente o tempo todo
- Sistemas de tipos modernos, como TypeScript, Haskell, Swift e Rust, oferecem por padrão distinção de null, tipos soma/union e inferência de tipos
- Com a popularização do autocompletar de métodos nas IDEs, a informação inserida na tipagem estática passou a trazer ganhos de produtividade além da checagem de erros
Hipótese sobre a mudança de popularidade da tipagem estática
- O fenômeno de a tipagem estática ter perdido popularidade nos anos 2000~início dos anos 2010 e voltado a crescer em meados/fim dos anos 2010
não aconteceu porque programação é uma indústria guiada por modas, mas porque a qualidade dos sistemas de tipos estáticos amplamente usados melhorou
A analogia da pá — a qualidade da ferramenta determina a escolha
- Ao cavar um buraco, se a pá for útil, naturalmente você a usará em vez das mãos;
mas, se a única pá disponível for feita de papel, ela só vai remexer a terra sem sentido, então é melhor usar as mãos - Em um sistema de tipos dinâmico, é preciso rastrear inteiramente na própria cabeça o estado e o conteúdo de variáveis e campos;
como o computador nem ajuda nem atrapalha, isso equivale a cavar com as mãos - Os sistemas de tipos estáticos fracos, comuns nos anos 1990 e início dos anos 2000, eram como uma pá de papel
- Não conseguiam ajudar nem mesmo com algo simples como distinguir ponteiros nullable e non-nullable
- Tinham apenas tipos produto (
product type) e não tinham tipos soma (sum type) - Havia o ônus de ter de escrever nomes de tipos manualmente em toda parte
- Código como
BufferedReader bufferedReader = new BufferedReader(new FileReader(filename));é um pequeno desastre
O que os sistemas de tipos modernos sempre oferecem
- Em contraste com sistemas de tipos modernos como TypeScript, Haskell, MyPy, Swift e Rust, eles sempre oferecem o seguinte
-
Distinção entre nullable e non-nullable
- Haskell oferece
Maybe t, TypeScriptT | null, SwiftT?e RustOptional<T> - O sistema de tipos informa com facilidade todos os pontos em que é necessário checar null e onde isso foi omitido
- Na prática, quase não se vê erros de ponteiro nulo em runtime
- Haskell oferece
-
Tipos soma ou union types
- Isso viabiliza a prática de "tornar estados inválidos impossíveis de representar (Make invalid states unrepresentable)"
- É possível criar objetos de máquina de estados (
state machine) com vários campos, e cada campo só existe quando o sistema está no estado relevante
-
Inferência de tipos
- Como o compilador pode entender
let x = 5;como um número, não é necessário escreverlet x: number = 5;
- Como o compilador pode entender
O ganho adicional trazido pela disseminação de recursos de IDE
- A popularização de recursos de IDE como autocompletar nomes de métodos aumentou a utilidade dos sistemas de tipos estáticos
- Nos anos 1990, o Intellisense do Visual Studio era um recurso matador, mas nos anos 2020 quase toda IDE ou editor oferece algo semelhante
- A informação colocada no sistema de tipos estáticos gera ganhos adicionais de produtividade, além da checagem de erros
Conclusão
- Um bom sistema de tipos dinâmico é melhor do que um sistema de tipos estáticos ruim
- Ainda assim, agora temos sistemas de tipos estáticos muito melhores do que no 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