1 pontos por GN⁺ 2023-10-05 | 1 comentários | Compartilhar no WhatsApp
  • 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 PersonId e PetId, 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ção
    • birthdayGreeting2(name, age) dá a pista de que existe nome e idade, mas não informa os tipos
    • birthdayGreeting3(name: string, age: number): string inclui 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 age precisa 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 Person
    • birthdayGreeting3(person: Person) facilita encontrar no IDE onde Person é usado
    • no caso sem tipos, birthdayGreeting2(person), só lendo o codebase inteiro para descobrir que de fato se espera um Person
  • 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} versus preson-{id}
    • tenta-se carregar dados de pessoa como tipo Pet
  • 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 como cache.get(PersonCacheKey::new(id)) que tente receber o resultado como Pet falha na compilação
  • IDs simples do tipo String também facilitam erros
    • do_something(id: String) não deixa claro qual ID deveria ser aceito
    • é possível cometer o erro de passar pet.id onde na verdade deveria ser passado pet.owner
  • A Svix usa um tipo separado para cada ID
    • PersonId(String)
    • PetId(String)
    • owner de Pet é 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
    • PetId não pode ser criado sem validação
    • assim, ao retornar 404 Not Found porque 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 422 ou 400

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
  • 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 vim versus emacs, 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

 
GN⁺ 2023-10-05
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

    • Acho que a maioria concordaria que tipos evitam muitos bugs, e neste thread também apareceu um estudo assim
      O ponto central está mais em quais são as razões subjetivas para concluir que não vale a pena investir em tipos
    • No fim, acho que teremos de nos contentar com uma questão de julgamento
      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
    • Penso quase a mesma coisa
      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
    • Tipagem estática é apenas uma fatia de queijo suíço para produzir software mais confiável
      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
    • Parece que se pensa que escrever programas em uma linguagem que expressa tipos faz “bugs de runtime” desaparecerem magicamente
      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”

    • Se a tipagem dinâmica realmente levasse a maioria dos desenvolvedores a escrever código simples, isso seria um argumento muito forte
      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
    • A frase “o sistema de tipos impede a compreensão de desenvolvedores iniciantes e só os faz eliminar marcações vermelhas” soa, à primeira vista, exatamente ao contrário
      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
    • Acho difícil entender esse raciocínio porque quase tudo nele me parece invertido
      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
    • A observação de que torres de marfim e pressão social podem fazer alguém relutar em adotar uma tecnologia é boa
      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”
    • Eu estava prestes a escrever praticamente a mesma resposta
      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

    • Sou o autor. Há algo sobre a forma como fazemos na Svix que mencionei só de passagem em um parágrafo e deveria ter explicado melhor
      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
    • Mesmo que nomes e endereços, ao olhar mais a fundo, sejam ambos strings, eles ainda devem ser usados não como strings, mas como tipos separados
      Misturar o campo de nome com o campo de endereço é quase sempre um erro, e o sistema de tipos pode impor isso
    • O fato de a maior parte dos dados trafegar como JSON realmente torna tudo tão confuso assim? Em certos casos, Map também é um tipo perfeitamente razoável
  • 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 grep na 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

    • Já vi definições de tipos em TypeScript completamente insanas
      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
    • Antigamente eu achava que, como dava para fazer edição interativa com from pdb import set_trace: set_trace(), era melhor interagir com o programa em execução do que com o compilador
      Mas, 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
    • Tenho muita curiosidade sobre como trabalham, na prática, as pessoas que lidam com mudança de assinatura de função desse jeito
      Existe alguma metodologia geral, tipo testes extremamente rigorosos com 100% de cobertura de código?
    • Sempre que vejo esse tipo de reclamação sobre tipagem estática, fico me perguntando como é que definiram os tipos e a arquitetura para isso virar um problema
      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
    • Não entendo muito bem como essas duas coisas se conectam à discussão sobre tipos
      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 grep para encontrar chamadas de função
  • A 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

    • Concordo totalmente. O argumento de manutenibilidade praticamente se encerra aí
      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
    • Também dá para argumentar que é justamente esse pensamento cuidadoso que torna o software mais limpo e melhor
      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

    • No fim, é uma discussão inevitavelmente frustrante, porque todo mundo tira conclusões completamente diferentes a partir da própria experiência
      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
    • Ver definições e variáveis no REPL e na IDE, com autocomplete e refatoração, é ótimo, mas só funciona quando você está de fato executando o código que quer inspecionar. Na minha experiência, esse método não escala bem
      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
    • Para mim, pelo contrário, essa parte foi muito mais fácil com tipagem estática
      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
    • Essa pode ser a sua experiência, mas não é a minha. Acho que não existe uma resposta certa para todo mundo. Escolha o que funciona para você e siga em frente
  • 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

    • C++, deixando de lado o sistema de tipos, é uma linguagem bem áspera para ter produtividade no geral
      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++
    • Em Python, programo principalmente em estilo funcional, mas mesmo assim tipos ajudam bastante
      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
    • Talvez você goste deste vídeo de alguns anos atrás da WWDC sobre programação orientada a protocolos (Swift): https://www.youtube.com/watch?v=p3zo4ptMBiQ
  • 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 birthdayGreeting do texto. O autor fica feliz porque, em birthdayGreeting("John", "20"), como "20" não é um número, a tipagem estática pega o bug. Mas birthdayGreeting(" ", 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á errado
    Isso é 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 tipo
    Se 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

    • Muitos dos exemplos podem perfeitamente ser pegos por tipagem estática, dependendo do sistema de tipos. Mas o ponto mais importante é a parte de que “bugs de tipo são trivialmente fáceis de pegar”
      É 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
    • Sou o autor. Acho que esses exemplos não são contraexemplos; pelo contrário, eles mostram justamente o meu ponto
      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ódigo
      birthdayGreeting("Anna" 4.5) e birthdayGreeting("Anna," -12335) são de fato válidos em JS, porque number é 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 invariantes
      Em 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
    • O que torna esses exemplos interessantes é que, a meu ver, o necessário é um tipo Name que represente um nome sempre válido e um tipo Age que represente uma idade sempre válida
      A validação fica em um único lugar, nos construtores desses tipos, e vários métodos como birthdayGreeting podem usar valores desses tipos sem se preocupar
      Nã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
    • Ao mencionar explosão de espaçonave, [1] me veio imediatamente à mente. Esse famoso acidente estava relacionado a verificação de tipos com faixas permitidas predefinidas para alguns tipos, e também se conecta com o seu argumento. Fazer birthdayGreeting aceitar um intervalo de 1 a 150 é algo fácil em ADA
      Há 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 entendo por que o fato de o compilador verificar apenas a corretude dos tipos, e não a corretude do programa, serviria como argumento para dizer que é perda de tempo
      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 auto
    Com 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 for de 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

    • Acho que o sistema de tipos do Python é bastante bom, considerando as circunstâncias
      Ele tem bons recursos, como Optional obrigar uma verificação antes de usar None, ou subtipagem estrutural via typing.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 feito
      O 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 dynamic em C#, ou fazer todos os métodos em Go receberem interface{}. Mas Python incentivou ativamente essa abordagem por muito tempo, e ainda hoje muitos tutoriais para iniciantes apresentam “receber *kwargs evita mudar a assinatura da função” como um recurso avançado para pessoas inteligentes, não como uma armadilha terrível
    • Os “sistemas de tipos” de Python e TypeScript foram projetados para introduzir tipos de forma gradual e em ambientes que não são greenfield
      Isso é essencial para migrações graduais, e é totalmente compreensível que funcionem desse jeito
    • Flavors e CLOS não são “sistemas de tipos”. São “sistemas de objetos”, e nem são particularmente feios
      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
    • O sistema de tipos do TypeScript é realmente surpreendentemente excelente. Eu gostaria que mais sistemas de tipos fossem tão expressivos
  • 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

    • Acho que “tipos reduzem bugs ao modificar uma base de código duradoura” não é algo muito controverso
      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
    • Como é uma afirmação muito forte, olhando para um caso extremo, há a camada intermediária do CompCert formalmente verificada: https://users.cs.utah.edu/~regehr/papers/pldi11-preprint.pdf
      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
    • Prefiro muito mais tipos estáticos fortes porque eles tornam o código autodocumentado
      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
    • O argumento dos tipos estáticos é que, justamente por serem estáticos, eles tornam bugs de tipo impossíveis
      Não há apego emocional nisso, exceto a irritação ao ouvir “mas eles não eliminam todos os bugs!”
    • Como você se sentiria se alguém afirmasse que “argumentos emocionais podem ser criados mais rapidamente do que argumentos racionais”?
      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