- Com mais de 20 anos de experiência escrevendo software, tipagem estática forte quase sempre vale a pena, exceto em casos como REPLs ou scripts descartáveis
- Os tipos deixam no código um contrato entre quem chama e quem é chamado, filtrando parâmetros ou valores de retorno incorretos em tempo de compilação ou de verificação de tipos
- O exemplo em que a string
"20", vinda de um input HTML, é usada como número e vira"201"mostra a diferença entre erros detectados antes da execução e erros expostos ao cliente - A Svix tenta colocar no sistema de tipos chaves de Redis, valores de cache, identificadores como
PersonIdePetId, e a validação de entradas de API para reduzir erros de digitação e envio de IDs incorretos - Omitir tipos pode acelerar a implementação inicial, mas aumenta os custos de documentação, testes e depuração; com inferência de tipos e suporte da IDE, refatoração e onboarding ficam mais fáceis
Por que insistir em tipos estáticos
- Tipagem estática forte, mais do que uma boa ideia, está próxima de ser o padrão correto para a maior parte do software
- Linguagens sem tipos ou suas variações também têm utilidade
- uso em REPL
- scripts pontuais em ambientes que já quase não têm tipos, por exemplo shell
- Fora isso, na maioria dos casos a preferência é por tipagem forte
- Não usar tipos pode até acelerar o desenvolvimento no curto prazo, mas isso se parece mais com “ir em velocidade máxima rumo ao precipício”
- No fim, a escolha se resume a uma de duas opções
- trabalhar mais e verificar invariantes em tempo de compilação ou de checagem de tipos
- trabalhar menos e verificar em tempo de execução, ou nem verificar em tempo de execução
- Erros em runtime nem sempre são detectados durante o desenvolvimento e, mesmo quando são, podem aparecer de uma forma visível para o cliente
- Testes ajudam, mas é difícil testar todos os tipos incorretos possíveis de parâmetros de função, e bloquear tipos errados com tipos é visto como o caminho mais fácil
Tipos se conectam diretamente a contratos no código e redução de bugs
- Tipos são comentários de código úteis tanto para pessoas quanto para ferramentas, além de um mecanismo que torna mais rígido o contrato entre trechos de código
- Mesmo com a mesma função de parabéns de aniversário, a clareza do contrato muda bastante
birthdayGreeting1(...params)nem sequer deixa claro quantos parâmetros existem, então é difícil entender o comportamento sem ler a documentaçãobirthdayGreeting2(name, age)dá a pista de que existe nome e idade, mas não informa os tiposbirthdayGreeting3(name: string, age: number): stringinclui no contrato os tipos de entrada e de retorno
- Se a função passar a usar
age + 1, a versão sem tipos terá problemas com entrada em string- valores vindos de inputs HTML podem sempre ser strings
birthdayGreeting2("John", "20")retorna"John will turn 201 next year!"- na versão tipada, como
ageprecisa ser numérico, a chamada incorreta falha na compilação
- O contrato entre quem chama e quem é chamado se torna mais importante à medida que o código cresce
- dá para saber como quem chama será afetado quando quem é chamado mudar
- isso é especialmente importante quando chamador e chamado são escritos por pessoas diferentes, como em bibliotecas open source
- Sem esse tipo de contrato, fica difícil saber até onde uma mudança vai causar impacto
Vantagens na experiência de desenvolvimento, refatoração e onboarding
- Informações de tipo são aproveitadas por IDEs e ferramentas de desenvolvimento para melhorar bastante a experiência de desenvolvimento
- Quando uma expectativa está errada, isso fica claro imediatamente enquanto o código está sendo escrito, reduzindo a carga cognitiva
- O desenvolvedor não precisa lembrar os tipos de todas as variáveis e funções no contexto atual, porque o compilador aponta o que não bate
- Refatorar também fica mais fácil
- ao mudar a implementação de uma função, o compilador pode avisar se alguma suposição em outro lugar foi quebrada
- Também fica mais fácil para um novo engenheiro se adaptar ao codebase ou a uma biblioteca
- seguindo as definições de tipos, dá para entender onde algo é usado
- como mudanças geram erros de compilação, fica mais fácil experimentar
- A diferença aparece no exemplo de uma função que recebe o tipo
PersonbirthdayGreeting3(person: Person)facilita encontrar no IDE ondePersoné usado- no caso sem tipos,
birthdayGreeting2(person), só lendo o codebase inteiro para descobrir que de fato se espera umPerson
- Dá para compensar parte disso com documentação, mas documentação envelhece facilmente, enquanto tipos viram documentação embutida no próprio código
- Tipos são vistos como uma forma mais forte de nomes de variáveis úteis
Como a Svix coloca informações no sistema de tipos
- A Svix tenta colocar o máximo possível de informação no sistema de tipos para reduzir erros que podem ser capturados em tempo de compilação e também melhorar a experiência de desenvolvimento
- Redis é essencialmente um protocolo baseado em strings e não tem tipos embutidos, então os benefícios dos tipos podem se perder na camada de Redis
- O exemplo de cache simples tem dois bugs
- há um erro de digitação no nome da chave, como
person-{id}versuspreson-{id} - tenta-se carregar dados de pessoa como tipo
Pet
- há um erro de digitação no nome da chave, como
- Para evitar isso, a Svix aplica duas estratégias
- exigir que a chave seja de um tipo específico, e não uma string genérica
- forçar o pareamento entre chave e valor
- Por exemplo, ao usar uma chave criada com
PersonCacheKey::new(id), um código comocache.get(PersonCacheKey::new(id))que tente receber o resultado comoPetfalha na compilação - IDs simples do tipo
Stringtambém facilitam errosdo_something(id: String)não deixa claro qual ID deveria ser aceito- é possível cometer o erro de passar
pet.idonde na verdade deveria ser passadopet.owner
- A Svix usa um tipo separado para cada ID
PersonId(String)PetId(String)ownerdePetéPersonId
- A validade do ID recebido pela API também é ligada à criação do tipo
- por exemplo, o ID de pet tem o formato de um prefixo
pet_seguido por um Ksuid PetIdnão pode ser criado sem validação- assim, ao retornar
404 Not Foundporque um pet não foi encontrado no banco de dados, é possível ter certeza de que o formato do ID em si era válido - IDs inválidos já são tratados no handler da API com
422ou400
- por exemplo, o ID de pet tem o formato de um prefixo
Argumentos contrários e o papel das ferramentas
- Os principais argumentos contra tipos são velocidade de desenvolvimento, curva de aprendizado e complexidade dos tipos, além do esforço e do boilerplate
- Fazer prototipação sem tipos certamente pode ser mais rápido
- é possível comentar código sem o compilador reclamar
- dá para colocar valores errados em campos até decidir quais são os valores corretos
- Mas isso é visto como dívida técnica agressiva e desnecessária, cujo custo será pago várias vezes ao depurar em ambiente local, suíte de testes e produção
- A curva de aprendizado existe, mas a maioria das pessoas não precisa virar especialista em tipos
- dá para trabalhar bem só com expressões de tipo simples
- quando travar, basta pedir ajuda
- Como desenvolvedores já precisam aprender muitas coisas, como programação, frameworks como React e Axum etc., o peso de aprender tipos é considerado exagerado
- Aprender tipos é um custo único, e os benefícios que eles trazem ao fazer onboarding em um codebase específico são maiores
- Sem tipos, é preciso bastante documentação e testes para obter estabilidade básica
- documentação e testes podem ficar desatualizados
- adicionar os tipos corretos é visto como exigir menos esforço
- Em linguagens sem inferência de tipos, tipar pode ser incômodo
- o exemplo em Java gera repetição, como
Person person1 = newPerson(); - o texto depois recebe uma correção dizendo que Java passou a ter inferência de tipos
- o exemplo em Java gera repetição, como
- Em linguagens com inferência de tipos, como Rust, fica mais conciso, como em
let person1 = new_person(); - Para obter os benefícios dos tipos, é preciso um editor de código ou IDE com recursos modernos de autocompletar que entendam a linguagem
- Diferentemente de discussões de preferência como
vimversusemacs, ou tabs versus spaces, a posição aqui é que os benefícios dos tipos em relação ao custo são tão grandes que é difícil entender por que não usá-los - Há um texto de continuação: using the type system effectively
1 comentários
Opiniões no Hacker News
O mais frustrante nessa discussão é que tudo gira em torno de como as pessoas se sentem, com pouca base empírica
As pesquisas existentes indicavam que não havia uma diferença significativa entre as duas abordagens e, a menos que existam estudos novos, é difícil afirmar que o lado preferido de cada um esteja claramente certo
Pessoalmente, gosto de linguagens tipadas, mas sistemas de tipos como o do TypeScript são insuficientes. Como não é possível usar os tipos de fato em runtime, bugs de runtime permanecem; e, como não dá para codificar muita lógica de runtime no sistema de tipos, ainda acabamos verificando manualmente casos impossíveis
Se o sistema de tipos praticamente eliminasse a necessidade de pensar em bugs de runtime, isso seria uma vantagem esmagadora, mas a maioria das linguagens não chega a esse nível e fica num meio-termo ambíguo entre overhead e alguns benefícios
Acho que a razão de não haver grande diferença em número de bugs ou velocidade é que, no fim, as coisas se compensam. Sem uma rede de segurança de tipos, você acaba testando mais; por outro lado, se confiar demais no sistema de tipos, sobra uma quantidade parecida de bugs de runtime. Eu gostaria que houvesse estudos sólidos sobre esse tema, mas é um problema difícil
O ponto central está mais em quais são as razões subjetivas para concluir que não vale a pena investir em tipos
Alguns anos atrás, analisei pesquisas sobre produtividade de desenvolvedores, e quase todas eram péssimas ou se aplicavam bem apenas a juniores. Por exemplo, iniciantes se beneficiam muito de feedback rápido sobre erros estáticos
É quase impossível aplicar um bom desenho experimental a profissionais, e não a estudantes universitários; além disso, é preciso isolar inúmeras variáveis como diferenças individuais, tipo de desenvolvimento e estilo de gestão, o que dificulta extrair um sinal. É triste, mas muitas coisas na vida são difíceis de medir de forma eficaz
O texto e muitos comentários falam de conveniência para o programador, produtividade e “correção”, mas as pesquisas atuais não trazem resultados significativos de que tipagem estática melhore ou piore essas coisas. Na prática, é algo subjetivo
Porém, há um efeito real da tipagem estática que é trivialmente demonstrável: ela permite escrever código mais eficiente. Isso deveria ser o centro da discussão sobre disciplina de tipos; o resto, neste estágio, é quase especulação
O TypeScript mencionado no texto, na verdade, não é fortemente tipado; ele é estaticamente tipado, mas fracamente tipado. Os tipos são quase anotações, sem garantias de desempenho ou de layout de memória. Por isso, fora documentação, você paga o custo da tipagem estática e obtém quase nenhum benefício substancial
É surpreendente que a comunidade técnica ignore evidências reais e aceite preferências culturais e pessoais como fatos
Como outras técnicas, ela tem buracos, então é preciso combinar várias técnicas para obter a máxima confiabilidade. Descartar tipagem estática porque ela não pega tudo é parecido com não trancar a porta porque um ladrão pode quebrar a janela. Se segurança realmente importa, você tranca a porta e também põe grades nas janelas; não escolhe apenas uma das duas coisas
Se a linguagem é poderosa o bastante para escrever programas gerais, também é poderosa o bastante para criar bugs
Tipagem estática pode ser eficaz para capturar certos tipos de bugs, mas não todos. Às vezes, ela melhora a legibilidade, como testes unitários estáticos ou uma linguagem específica de domínio para documentação executável
Em geral, linguagens dinâmicas são mais ágeis e permitem escrever mais testes com mais facilidade. Há testes que você não precisaria escrever se estivesse usando uma linguagem de tipagem estática, então tipos continuam sendo úteis, mas não são tão universalmente poderosos quanto se costuma acreditar
Separadamente da pressão social para gostar de tipos estáticos, no fim o motivo pelo qual sempre me afastei deles foi o fato de que, ao redor deles, sempre se construiu uma torre de marfim
Passei 10 anos criando software em cada um dos dois paradigmas e hoje prefiro não usar sistemas de tipos
Tipagem dinâmica me parece uma força que obriga a escrever código simples, assim como testes unitários obrigam a escrever código componível. Código fácil de ler e fácil de entender
Também não acho muito boa a afirmação de que desenvolvedores iniciantes passam a acessar a codebase com mais facilidade. Isso porque ela pode facilmente incentivar um ciclo repetitivo de apenas eliminar marcações vermelhas sem entender. O sistema de tipos faz com que, em cada projeto, a pessoa tenha de aprender, por cima da linguagem, mais uma linguagem altamente específica do domínio, o que muitas vezes atrapalha a compreensão do comportamento real
Os problemas apresentados no texto podem ser resolvidos de maneiras tão robustas quanto tipos, mas mais fáceis de entender. É possível usar tipos de forma simples, mas, pela minha experiência, na prática quase nunca foi assim. Também não gosto de autocompletar, então entenda dessa forma
Talvez eu seja apenas um desenvolvedor velho gritando “código é documentação”, mas isso também pode vir de uma insatisfação profunda com a abundância atual, no setor, de desenvolvedores do tipo “o ChatGPT disse que está certo e eu ganho um salário alto”
Mas, em geral, as evidências em contrário parecem fortes. Tipos criados depois para documentar código dinâmico real costumam ser muito mais complexos do que a mesma funcionalidade implementada com tipagem estática desde o início. O ecossistema TypeScript, com o DefinitelyTyped, oferece inúmeros exemplos
É difícil dizer que esses tipos sejam “usados de forma simples”, mas essa complexidade não vem do sistema de tipos em si nem da forma como as definições de tipos são fornecidas; ela vem da complexidade do código dinâmico que esses tipos descrevem
Pacotes equivalentes feitos com tipagem estáática desde o começo geralmente têm interfaces mais simples. Isso porque os tipos são definidos antes, em vez de serem encaixados depois em uma API existente
Eu chegaria a dizer que, sem explicitar uma interface, não dá para saber se ela é simples ou complexa. Concordo com o ideal de que “código é documentação”, mas, se não há código que explicite a interface, então essa interface é, por definição, pouco documentada
Eu até gostaria de ver uma cena dessas de perto. Na minha área, lógica específica de domínio é quase impossível de entender em codebases com tipagem dinâmica, enquanto código com tipagem estática ensina a lógica de negócio ao desenvolvedor
A frase “código é documentação” também me deixa confuso. Pela minha experiência, é preciso haver tipos estáticos para que o código seja documentação. Sem eles, não há como saber quais propriedades um objeto tem ou por que se está verificando uma propriedade que eu achava que nem existia. Existem comentários, mas quase nunca vejo alguém deixar comentários significativos
Minha experiência é a oposta. Padrões muito dinâmicos são difíceis de tipar corretamente, e um bom sistema de tipos incentiva padrões mais simples, o que também torna os tipos mais simples
Eliminar marcações vermelhas é importante. Uma marcação vermelha significa que há um problema, e isso é muito mais fácil do que descobrir erros por outros meios. Não entendo por que alguém gostaria de descobrir esse erro só mais tarde
A afirmação de que também não gosta de autocompletar me coloca do lado de quem desconfia dos opositores da tipagem estática. Um programador que não quer que o computador ajude na programação é muito suspeito
Mas isso não diminui seus méritos técnicos. Uma tecnologia pode ser excelente e, ainda assim, ter pessoas presunçosas ao seu redor
A afirmação de que a tipagem dinâmica leva a escrever código simples soa como dizer: “dirigir vendado é bom porque faz você dirigir devagar”. Se esse é o objetivo, basta usar um linter que limite o tamanho das linhas ou o número de parâmetros; não é preciso criar a restrição de forma indireta
Tipos não são a única solução, mas vejo neles a primeira ferramenta a pegar porque a relação entre investimento e retorno é muito alta. O investimento é quase nulo e o ganho é grande
Concordo com “código é documentação”, mas tipos também fazem parte do código. Então eu preferiria dizer: “código é documentação, e tipos fazem parte do código”
A área em que estamos é engenharia, não existe uma única resposta correta e tudo é uma questão de trade-offs. É justamente por isso que nosso trabalho não será simplesmente automatizado e eliminado de imediato
O clima nesta thread de “olhar por cima” por causa da opinião ou experiência de outro engenheiro é realmente desagradável
Em um cenário em que a maior parte dos dados trafega pela rede como JSON, a batalha para aplicar tipagem estática forte costuma ser travada de forma bastante inconsistente
Devemos usar todas as ferramentas disponíveis, mas a maior parte dos “dados” é muito mais fluida do que parece. As pessoas mantêm números de telefone como strings não por preguiça, mas porque, em algum momento, acharam que poderiam transformá-los em um tipo mais forte e apanharam demais por isso. O mesmo vale para nomes, endereços e CEPs
Esses valores precisam ser recebidos de usuários e, na prática, não há muito além de fazer parsing de texto. Se você construir um sistema que não preserve o texto original antes do parsing, algum dia quase certamente vai se arrepender
Acho que a melhor abordagem é manter o texto original de entrada e oferecer aos usuários do backend uma camada que o apresente como um conjunto de dados tipado, mas é preciso avaliar se esse investimento compensa em cada pequena área
Se houver uma avaliação pesada, provavelmente será necessária uma camada que converta para SAT ou outro modelo numérico. Nesse mundo, números são a abstração. Tentar fazer de outro jeito quase certamente causa sofrimento. É melhor ter uma camada que traduza o problema para uma formalização e o espaço de soluções para o domínio; tipos podem ajudar nisso, mas, com frequência demais, os “tipos” que realmente recebem atenção não são desse tipo
Graças a bibliotecas como Serde e Pydantic, seguimos a abordagem de que desserialização é validação. Validamos todos os dados JSON antes de transformá-los em structs do código
É parecido com o exemplo do Redis: mesmo recebendo JSON pela rede, depois de validá-lo completamente, quando ele chega ao código você pode ficar tranquilo de que é um tipo bem formado. Assim, no código, é possível assumir que um tipo de e-mail é um e-mail válido e que um tipo de ID é um ID válido
Misturar o campo de nome com o campo de endereço é quase sempre um erro, e o sistema de tipos pode impor isso
Se um typo vira erro em runtime, isso não é “mover-se mais rápido”; e, se ao mudar a assinatura de uma função você precisa dar
grepna base de código para encontrar todos os pontos de chamada e rezar para ter corrigido tudo, isso também não é “mais produtivo”Tipos são bons, mas qualquer coisa em excesso vira problema. Se você transformar em objetivo de vida codificar toda a lógica de negócio no sistema de tipos, vai criar uma confusão mais incompreensível do que não ter tipo nenhum. Se o nome de um tipo não cabe em uma linha na mensagem de erro, você foi longe demais
Para ser justo, era para compatibilizar com código JS puro legado, e aquela pobre variável podia conter todo tipo de valor
Sou eternamente grato ao TypeScript, mas não ficaria surpreso se, no futuro, um artigo sobre “a era em que os tipos foram longe demais” trouxesse esse tipo de código
from pdb import set_trace: set_trace(), era melhor interagir com o programa em execução do que com o compiladorMas, quando o programa fica só um pouco mais complexo, a situação muda. Quando você começa a passar dados entre sistemas por filas, usar assíncrono, threads e multiprocessamento, e empregar bibliotecas binárias compiladas em partes críticas de desempenho, no fim acaba desejando ter escrito tudo em Erlang
Existe alguma metodologia geral, tipo testes extremamente rigorosos com 100% de cobertura de código?
Se no build ou na compilação você não consegue capturar os pontos de chamada, então não está usando tipagem estática
Typos podem gerar código errado até na linguagem de tipagem estática mais forte. Caso contrário, o que significaria escrever código? O que é pior: um erro em runtime ou um resultado errado sem erro nenhum?
Executar um projeto Python pode ser mais rápido do que compilar C++, e linguagens de tipagem dinâmica também podem oferecer meios melhores do que
greppara encontrar chamadas de funçãoA ideia de que não usar tipos traz a vantagem de desenvolver mais rápido também não bate com a minha experiência. Tipagem estática torna a programação do dia a dia mais rápida
A melhora da IDE graças à tipagem estática foi tratada mais adiante, mas eu sinto isso também no REPL. Erros de tipo capturados estaticamente dão mensagens de erro significativas, muito mais próximas da causa raiz real, e permitem corrigir mais rápido do que erros em runtime
Também reduzem o peso de ter que pensar com cautela demais sobre tipos. Como o compilador mantém a disciplina, eu posso me preocupar menos. Dá para avançar mais rápido com a confiança de que uma grande classe de erros será capturada imediatamente
Na minha experiência, sistemas de tipos estáticos são fáceis de usar, aceleram o desenvolvimento e aumentam a confiabilidade. Até agora, os custos foram só dois: podem ser mais difíceis de aprender e mais difíceis de implementar
O desenvolvedor júnior que você contratar daqui a 6 meses vai demorar muito mais para se adaptar a código sem tipos
Admito que, para algumas pessoas, escrever inicialmente pode ser “mais rápido”, mas depois todo desenvolvedor que tiver que ler aquele código fica mais lento
Pode ser bom pensar exatamente no que entra e no que sai, e por quê, em vez de produzir uma bagunça que apenas passa no compilador
Acho que o autor está errado em quase todos os pontos. Eu também pensei assim por décadas, mas mudei completamente de ideia nos últimos anos
Tipos reduzem bugs? Não. Talvez um pouquinho, mas nada significativo. Basta olhar os estudos relacionados
Tipos dão uma experiência de desenvolvimento melhor? Não. Meu REPL e minha IDE têm todas as definições e variáveis. Consigo fazer autocomplete de todos os símbolos, árvore de chamadas, navegação de usos, refatoração com confiança, além de executar, substituir e encapsular funções isoladamente, tanto no REPL quanto dentro da aplicação
Codificar tudo no sistema de tipos? Impossível. É preciso validação em runtime
Boa sorte também para desfazer definições de tipos quando os requisitos mudarem. Esse é o golpe decisivo. Tipagem estática cristaliza cedo demais o modelo de dados do domínio conforme entendido no momento. Esse modelo muda e, se você der azar, terá que dar suporte a várias variações de modelos de domínio dentro do mesmo runtime. Se tiver usado herança, pior ainda
É verdade que tipagem estática dá uma grande alavanca para otimizações do compilador, mas há linguagens de tipagem dinâmica que oferecem tipagem estática como recurso opcional
Em muitos casos de uso, especialmente no desenvolvimento corporativo, uma linguagem funcional de tipagem dinâmica e com imutabilidade em primeiro lugar traz grandes ganhos no longo prazo
O erro de categoria que defensores ferrenhos de tipos costumam cometer é presumir que se escreveria o mesmo código, só que sem tipos. Na prática, não é assim que se escreve
Em quase todos os pontos, minhas conclusões são exatamente o oposto. Claro que é verdade que validação em runtime é necessária, mas a maior parte dela pode ser evitada
Quanto a mudanças de requisitos, vejo a tipagem estática como algo que, pelo contrário, facilita a adaptação. Nos sistemas de tipagem dinâmica que encontrei, pressupostos importantes sobre estruturas de dados estavam espalhados por toda parte; às vezes eram verificados dinamicamente como pré e pós-condições, às vezes existiam só nos testes, ou nem eram verificados
Para mudar requisitos, era preciso inferir todos os impactos sobre esses pressupostos implícitos, e isso tornava as mudanças assustadoras. Subir a aplicação com o código novo era fácil; saber se eu tinha quebrado algum caminho raro de código que não tinha imaginado era muito difícil
Prefiro muito mais uma etapa de análise estática que me diga: “você mudou esta interface; sabia que este caminho de código dependia dessa parte?”. Tipagem estática não é o único jeito, mas acho que dá muito menos trabalho do que montar validação dinâmica e testes no mesmo nível
Um sistema de tipos não elimina a validação em runtime, mas, quando usado corretamente, a reduz drasticamente
Quando os requisitos mudam, a melhor parte é o compilador dizer exatamente o que precisa ser corrigido para voltar a funcionar. Ao fazer a mesma coisa em uma linguagem dinâmica, você precisa rastrear manualmente, esperar que os testes unitários falhem e rezar para que nenhum caminho tenha escapado
Eu conseguia encontrar com muito mais confiança todos os lugares em que um tipo específico era usado e ver, em cada um, se a mudança era necessária. Em um ambiente de tipagem dinâmica, esse trabalho era muito mais miúdo e trabalhoso
Mesmo como desenvolvedor que já escreveu centenas de milhares de linhas em C++, Python e JS, não sei bem. Não é tão claro assim
Sou produtivo nos três, mas Python geralmente vence. Só que eu não escreveria um motor de jogo ou um codec de vídeo em Python
JavaScript é inconsistente e esquisito, mas o legado da Netscape já nos prendeu todos a ele
Em um estilo muito orientado a objetos, com muitas classes aninhadas enormes, tipos estáticos no momento da compilação/análise podem reduzir muitos erros. Mas passei a ver orientação a objetos, em geral, como algo próximo de um desastre, e funções simples com dados estruturados quase sempre vencem em simplicidade e manutenibilidade
Servidores de linguagem e IDEs modernos conseguem detectar muitos erros de tipagem também durante o desenvolvimento em JS/Python. Sou exigente com muitas partes da programação, mas nunca tive uma posição forte sobre tipagem estática versus dinâmica. Ambas têm milhões de projetos bem-sucedidos
Acho que o maior ganho de produtividade que uma linguagem pode oferecer é coleta de lixo. Tenho mais curiosidade em ver como Go, com sintaxe e tipos muito mais simples, se compararia. Java também pode ser melhor. Embora seja verboso, na maioria das tarefas a carga cognitiva é apenas uma fração da de C++
Cometo muitos erros de digitação e também erro com frequência a ordem dos argumentos. Especialmente ao trabalhar com aprendizado de máquina, tipos ajudam muito. Gastar 30 minutos no processamento de dados e depois ver o código de treinamento morrer é uma das coisas que mais quero evitar
Vejo a tipagem gradual do Python como um meio-termo muito bom entre prototipagem rápida e adicionar anotações de tipo quando uma função já está suficientemente madura
A discussão sobre tipos fortes serem melhores que tipos fracos já foi resolvida, mas ainda não está resolvido se tipos estáticos são melhores que tipos dinâmicos
Defensores de tipos estáticos acham que o compilador deve verificar invariantes de tipo para confirmar a “corretude”, enquanto defensores de tipos dinâmicos veem isso como perda de tempo
Eu definitivamente fico no segundo grupo. Porque o compilador só consegue verificar a corretude dos tipos, não a corretude do programa. A corretude dos tipos é necessária para a corretude do programa, mas não é suficiente. Defensores de tipos estáticos não conseguem admitir isso e se iludem achando que tipos estáticos garantem mais do que realmente garantem
Veja o exemplo
birthdayGreetingdo texto. O autor fica feliz porque, embirthdayGreeting("John", "20"), como"20"não é um número, a tipagem estática pega o bug. MasbirthdayGreeting(" ", 123)não é pego." "não é um nome.birthdayGreeting("Anna," -12335)também não é pego. Por outro lado,birthdayGreeting("Anna" 4.5)é pego, embora 4,5 também possa ser considerado uma idade, então dá para dizer que isso é que está erradoIsso é importante. “Bugs de tipo” são trivialmente fáceis de pegar, mas bugs semânticos podem ficar escondidos por anos. É o caso de overflow no saldo de uma conta armazenado como
uint, de um número que deveria ser primo em determinada posição mas não é, ou de uma lista que não deveria estar vazia. Nem mesmo tipos dependentes conseguem garantir invariantes desse tipoSe isso parecer difícil de acreditar, basta procurar grandes bugs que causaram explosões de espaçonaves ou acidentes de carro. Até onde sei, nenhum caso teve como causa um erro de tipo de verdade, e a esmagadora maioria foram erros semânticos
[1] A maioria não entende que tipos devem ser vistos em pelo menos dois eixos, forte/fraco e estático/dinâmico, e continua confundindo tipagem fraca com tipagem dinâmica. C é estática e fracamente tipada, Python é forte e dinamicamente tipada, e JavaScript é fraca e dinamicamente tipada
É exatamente por isso que eu fico do lado da tipagem estática. É tão trivial que dá para tratar de forma declarativa, bem ao lado do código a ser verificado, com feedback imediato, em todos os pontos de chamada e em todas as subexpressões e instruções
Anotações de tipo não significam que a semântica ou a lógica de domínio estejam corretas, e isso ainda precisa ser testado. Mas elas podem substituir dezenas de testes triviais que são ortogonais à lógica que importa. Sinceramente, quase ninguém escreve todos esses testes sem deixar lacunas
Mais adiante no texto eu disse que, ao criar tipos em lugares como entrada de usuário, fazemos validação. Portanto, o tipo
Nameé sempre válido, e" "não é um nome. Como o tipo garante um nome válido, isso certamente seria pego na nossa base de códigobirthdayGreeting("Anna" 4.5)ebirthdayGreeting("Anna," -12335)são de fato válidos em JS, porquenumberé ponto flutuante. Só que, ao escrever o texto, eu estava pensando em inteiros. É mais um caso em que um tipo mais rigoroso que o de TS, por exemplo Rust, ajuda a definir melhor os invariantesEm resumo, ao tentar mostrar um exemplo simples, não defini todos os tipos com o rigor que costumo usar, e com isso acabaram aparecendo ainda mais bugs que teriam sido pegos por tipos
Nameque represente um nome sempre válido e um tipoAgeque represente uma idade sempre válidaA validação fica em um único lugar, nos construtores desses tipos, e vários métodos como
birthdayGreetingpodem usar valores desses tipos sem se preocuparNão sei como implementar bem esse padrão sem verificação de tipos ou, no mínimo, hints de tipo opcionais e análise estática. Em vez disso, validar as entradas em todos os métodos é oneroso demais, e assumir que o chamador vai passar valores válidos e depois testar para evitar grandes acidentes também não é satisfatório
birthdayGreetingaceitar um intervalo de 1 a 150 é algo fácil em ADAHá alguns problemas públicos relacionados a espaçonaves que provavelmente poderiam ter sido pegos com uma verificação de tipos melhor. Conversão entre sistema métrico e imperial também poderia ser resolvida colocando unidades nos tipos. Dito isso, no caso de [2], é bem provável que o problema estivesse mais do lado dos testes de integração
É claro que verificação de tipos não consegue encontrar todos os problemas de código, especialmente problemas de algoritmo, e também não substitui testes. Ainda assim, durante o desenvolvimento, feedback imediato e hints de tipo são extremamente valiosos
No exemplo, a string do nome também poderia ser trocada por um tipo ou objeto pessoa
[1]: https://en.m.wikipedia.org/wiki/Ariane_flight_V88
[2]: https://en.m.wikipedia.org/wiki/Mars_Climate_Orbiter
Não existe solução perfeita, mas existem muitas soluções valiosas
Houve uma convergência considerável sobre esse problema. Hoje, a maioria das linguagens oferece algum grau de inferência de tipos no nível das instruções. C++ também tem
autoCom isso, o boilerplate de tipos no código diminuiu bastante. Já passou a época em que era preciso escrever por extenso aqueles tipos longos de iteradores em um
forde C++Declarações de funções e campos de structs são lugares em que a informação de tipo é necessária para ler o código. Quando um programa passa de algumas centenas de linhas, ou há mais de um desenvolvedor, algum nível de anotação é indispensável
A oposição principal vem, naturalmente, de usuários de Python e JavaScript. Python recebeu posteriormente um sistema de tipos consultivo bem estranho, e JavaScript recebeu o TypeScript posteriormente. Ambos são sistemas de tipos adicionados por cima e usados em ambientes em que código tipado e não tipado se misturam. Isso é doloroso
LISP também recebeu posteriormente, décadas atrás, um sistema de tipos com “flavors” e o Common LISP Object System, e aquilo também não era bonito. A lição é que adicionar um sistema de tipos depois vira uma bagunça
Ele tem bons recursos, como
Optionalobrigar uma verificação antes de usarNone, ou subtipagem estrutural viatyping.Protocol. Teria sido melhor se Python tivesse sido projetado com tipos em mente desde o início, mas, considerando a exigência de integrar com o código Python existente e não quebrar nada, foi um trabalho bem feitoO problema maior dos tipos estáticos em Python é o ecossistema e as convenções. Isso ficou ainda pior especialmente porque muitos desenvolvedores usam Python, na prática, como cientistas de dados. Por preguiça de escrever assinaturas de métodos adequadas, abusam de
*args/**kwargsÉ muito comum métodos passarem DataFrames ou dicionários como sacolas de bugigangas. Pontos extras se o método adiciona ou remove colunas ou campos, de modo que você só descobre o que há dentro dessa sacola de dados ao executar o código ou ler todas as linhas
Claro que dá para fazer algo parecido em quase qualquer linguagem. É possível usar todos os tipos como
dynamicem C#, ou fazer todos os métodos em Go recebereminterface{}. Mas Python incentivou ativamente essa abordagem por muito tempo, e ainda hoje muitos tutoriais para iniciantes apresentam “receber*kwargsevita mudar a assinatura da função” como um recurso avançado para pessoas inteligentes, não como uma armadilha terrívelIsso é essencial para migrações graduais, e é totalmente compreensível que funcionem desse jeito
Flavors foi introduzido em Lisp, que não tinha sistema de tipos; depois, CLOS foi adicionado ao Common Lisp, que era um Lisp que já tinha sistema de tipos
As pessoas sempre foram fanáticas com aquilo a que se agarram emocionalmente, mais do que racionalmente
A afirmação de que “tipos reduzem bugs” é mais algo que parece plausível do que algo verdadeiro
https://blog.metaobject.com/2014/06/the-safyness-of-static-t...
Não foi por falta de tentativas. Dá até para dizer que essa afirmação foi refutada
Ainda assim, pessoalmente gosto de tipos estáticos. Principalmente pelo efeito de documentação; e, talvez não por acaso, esse também é o único efeito positivo para o qual há evidência empírica realmente sólida
Impedir regressões pode ser mais importante do que escrever o código correto de primeira, e esse aspecto não é avaliado quando se olha para código que não evolui
Concretamente, remover campos de objetos em um grande projeto JavaScript puro é, por natureza, um campo minado, e no passado causou muitos bugs. Já em um projeto totalmente em TypeScript, dá para fazer a mesma mudança com confiança
Acho que isso pode ser visto como evidência empírica de que tipos estáticos muito fortes reduzem bugs
No fim, como muitos comentários dizem, tipos não são uma questão binária de sim/não, mas um grande espectro em vários eixos, como estático/dinâmico e forte/fraco. Também há grandes diferenças entre sistemas de tipos, e grandes diferenças na forma como as pessoas aplicam esses sistemas de tipos aos problemas
Mesmo em uma linguagem de tipos estáticos e fortes, dá para representar tudo como strings e ficar convertendo o tempo todo; isso é, na prática, trabalhar como em uma linguagem de tipos dinâmicos. Por outro lado, ao aproveitar as ferramentas que o sistema de tipos oferece para criar classes que representam valores legais e declarar invariantes importantes, é possível obter benefícios
A produtividade aumenta não só várias vezes, mas por ordens de grandeza. Digo isso como alguém que usou bastante várias linguagens que cobrem uma ampla parte desse espectro, como C, C++, Java, Python, JavaScript e TCL
Fica muito mais fácil raciocinar sobre código em que não mexi recentemente, seja do projeto atual ou de uma dependência. Não preciso ficar me desviando para descobrir exatamente o que posso fazer com o objeto retornado por uma função, então consigo me concentrar mais no problema em questão
Também há aquela sensação agradável de alívio quando a compilação passa, mas isso é secundário
Não há apego emocional nisso, exceto a irritação ao ouvir “mas eles não eliminam todos os bugs!”
Essa discussão parece exatamente isso
Eu só quero a racionalidade simples de o compilador dizer “não” quando tento tratar um hashmap como Apple ou String