2 pontos por GN⁺ 2025-01-26 | 1 comentários | Compartilhar no WhatsApp
  • A cultura de desenvolvimento em que instalar pacotes virou o padrão aumenta o dependency churn, que leva a atualizações, patches, auditorias e gestão de dependências transitivas, criando um custo oculto para a produtividade
  • O impacto é maior em ecossistemas com empacotamento bem estabelecido, como JavaScript e Rust; há exemplos como 28 crates em um novo projeto Tokio, 172 crates em Rocket e 142 dependências no CLI do MiniJinja
  • Mesmo funcionalidades antigas e estáveis, como terminal_size, acabam, paradoxalmente, incorporando crates adicionais e lançamentos repetidos por causa de mudanças em bibliotecas de abstração de plataforma
  • Para recursos pequenos, criar uma implementação sem dependências com ChatGPT ou Cursor pode ser mais rápido do que buscar dependências e mantê-las atualizadas, além de reduzir o peso de compilar código externo
  • Bibliotecas para áreas difíceis como HTTP, QUIC, gráficos e tokio são necessárias, mas aceitar um grande grafo de dependências por causa de uma única função deve ser uma escolha mais questionada

O treadmill de manutenção criado pelo crescimento das dependências

  • Desenvolvedores instalam pacotes com facilidade em nome da produtividade, mas o resultado é ficar preso ao dependency churn: um fluxo interminável de atualizações, patches, auditorias e gestão de dependências transitivas
  • Quanto mais bem resolvido é o empacotamento em um ecossistema, mais esse problema se destaca; JavaScript e Rust são especialmente afetados
  • Exemplos em Rust:
    • Um novo projeto Tokio traz 28 crates
    • Um novo projeto Rocket chega a 172 crates
    • O MiniJinja em si pode existir com uma única dependência, mas sua variação CLI traz 142 dependências
  • O problema central é uma estrutura que obriga a compilar e gerenciar muito mais código externo do que o realmente necessário para uma pequena funcionalidade

O paradoxo do código estável mostrado por terminal_size

  • terminal_size é, como o nome diz, um crate para descobrir o tamanho do terminal
  • A API de base usada por essa funcionalidade é efetivamente estável desde os primórdios dos terminais de computação, mas, dependendo do sistema operacional, ela traz 3 a 4 crates adicionais
  • Surge uma situação em que milhares de outras funções são compiladas apenas para verificar se o terminal é 80x25 ou 120x40
  • Esse crate teve 26 lançamentos, mas uma implementação própria colocada em um projeto de 10 anos atrás para a mesma funcionalidade ainda funciona sem atualizações
  • O motivo de tantos lançamentos não é a mudança da funcionalidade em si, e sim as mudanças contínuas nas bibliotecas de abstração de plataforma subjacentes
  • No UNIX, há a exceção de precisar da dependência libc
    • Isso ocorre porque Rust não expõe as constantes libc da plataforma, e essas constantes não são padronizadas
    • Ainda assim, libc é uma dependência comum e leve, difícil de evitar

Segurança e cultura de reutilização de código reforçam as dependências

  • A abordagem do tipo “big supply chain” desconfia de copiar funções para dentro do projeto ou usar unsafe diretamente, pressionando para delegar isso a camadas de abstração de plataforma
  • Também existem empresas que fornecem ferramentas para lidar com problemas de dependências e, em nome da segurança, incentivam manter dependências e atualizá-las para as versões mais recentes
  • Porém, muitas dependências em si podem se tornar uma fonte importante de problemas de segurança
  • O objetivo do código deveria ser chegar, em algum momento, à estabilidade, de modo que não precise mais de atualizações
  • No ecossistema Rust, mesmo uma dependência que funcione de forma estável pode receber uma avaliação baixa no RUSTSEC se o bug tracker estiver um tanto inativo
  • A cultura corporativa de code review também influencia o open source
    • Um engenheiro que traz uma biblioteca nova e brilhante tem mais chance de ser recompensado do que repreendido
    • Como resultado, surgiram ferramentas como o Dependabot, e os projetos passam a receber PRs contínuos de atualização de dependências
    • Dentro das empresas, apenas vendoring, auditorias internas e upgrades em toda a organização já podem manter equipes de engenharia constantemente ocupadas

Funcionalidades pequenas podem ser feitas diretamente

  • O caminho mais simples é escrever você mesmo o código necessário
  • O trabalho inicial pode ser maior, mas, uma vez escrito, o código não precisa de novos crates nem de esperar que o autor upstream corrija edge cases
  • Se o código quebrar no seu caso de uso, você mesmo corrige; código que funciona não precisa necessariamente entrar no treadmill de manutenção
  • Em 2025, ChatGPT ou Cursor conseguem gerar rapidamente uma implementação sem dependências para funcionalidades pequenas e comuns
  • Muitas funções pequenas têm baixo overhead de manutenção e podem pesar menos do que upgrades contínuos de dependências
  • Se o código tem poucas linhas, não é preciso compilar milhares de linhas de outra pessoa por causa de uma única funcionalidade

Valorizar mais dependências baixas

  • Nem toda dependência é ruim
    • Bibliotecas gráficas que abstraem drivers complexos
    • Implementações de protocolos como HTTP e QUIC
    • Bibliotecas importantes como tokio, que não podem ser removidas e que ninguém pretende remover
  • Mas, se você usa só uma função e compila centenas de funções, isso deve ser visto como um sinal de alerta
  • Devemos reconhecer mais a escolha de escrever pequenas funções diretamente para evitar o grafo de dependências transitivas
  • É preciso desconfiar mais de grandes grafos de crates e enxergar de forma positiva código simples e estável que pode passar anos sem ser tocado
  • sha1-smol originalmente se tornou, com o nome sha1, o crate padrão para calcular hashes SHA1, mas depois cedeu o nome ao rust-crypto e recebeu pressão para se adequar ao ecossistema crypto maior
    • Usar o novo crate sha1 traz 10 dependências
    • Isso foi difícil de evitar porque o nome no registry era importante e havia exigências de compatibilidade com traits
  • O MiniJinja destaca suas poucas dependências no README
$ cargo tree
minimal v0.1.0 (examples/minimal)
└── minijinja v2.6.0 (minijinja)
    └── serde v1.0.144
  • Há um PR no MiniJinja para remover a última dependência
  • Nas situações adequadas, devemos celebrar fazer você mesmo e dar mais reconhecimento a autores que criam bibliotecas open source com poucas ou nenhuma dependência

1 comentários

 
GN⁺ 2025-01-26
Opiniões no Hacker News
  • Gosto da linguagem Rust em si, mas não gosto do ecossistema de dependências do Rust. Muita gente reclama que é difícil adicionar dependências em C++, mas, para mim, isso acaba parecendo uma funcionalidade. Porque faz você pensar se aquilo é realmente necessário.
    Em C++, eu controlo as dependências; em Rust, elas passam rapidamente de 100 e você acaba desistindo. Do ponto de vista de segurança, sinceramente não dá para saber o que estou distribuindo.
    Além disso, Rust não tem compatibilidade de ABI nem uma cultura de bibliotecas compartilhadas. Por isso, dá a impressão de quebrar o modelo de distribuição de pacotes dos sistemas operacionais. Ao escolher uma distribuição Linux, você está confiando nas pessoas que constroem aquela distribuição; Rust parece mais próximo do Python, em que qualquer um pode publicar algo no PyPI.

    • Debian/Ubuntu podem ser considerados seguros porque os mantenedores assinam os pacotes, e esses mantenedores são verificados. No fim, é uma estrutura baseada em acreditar que Debian/Ubuntu só permitem que pessoas confiáveis assinem pacotes.
      E Docker/Python/Rust? Não faço a menor ideia de quem criou minha imagem Docker, meu pacote PyPI ou meu crate Rust.
      Na prática, voltamos à época em que se trocavam EXEs e DLLs em arquivos ZIP. Só que agora chamamos isso de contêiner e o executamos orgulhosamente como root.
      Como o autor disse, às vezes o melhor é simplesmente incorporar o código-fonte das dependências. Antigamente isso era chamado de vendoring, e era bem importante em Rails/Ruby. Tem a grande vantagem de não ser afetado por uma tomada maliciosa de um pacote mais tarde e, se quiser, você pode mesclar manualmente os patches de segurança do upstream.
    • Não sei que tipo de trabalho fazem as pessoas com essa visão, para conseguirem reinventar a roda em todo projeto só porque têm medo de riscos hipotéticos. A maior parte desses riscos continua existindo do mesmo jeito em reimplementações internas, e os bugs também.
      Não me vem à cabeça nenhum projeto em que a síndrome de NIH tenha tido efeito líquido positivo. Mesmo dependências não essenciais economizam muito tempo.
      Se você recria, em todo projeto, parte das dependências opcionais, fico curioso para saber de onde vem o tempo para corrigir bugs adicionais, casos de borda, várias versões de dependências transitivas e suporte a múltiplas plataformas.
    • Quando uma linguagem já nasce com um gerenciador de pacotes que reduz muito o atrito para adicionar e distribuir pacotes, o ecossistema da linguagem perde algo muito útil: uma boa pressão de seleção natural.
      Se isso se combina com repositórios como PyPI, npm e Cargo, em que qualquer um pode publicar e há pouca curadoria, além de uma biblioteca padrão pequena, é bom se preparar para sofrer.
    • Fico me perguntando se o recurso de ter “um conjunto de bibliotecas fáceis de importar” vira um antirrecurso justamente por facilitar a importação de bibliotecas de apoio. Isso parece mais uma questão de escolha do desenvolvedor do que de Rust ou JS.
      Quem escolhe quais dependências importar é o desenvolvedor. Dependências que ninguém usa vão desaparecer, e o motivo de a maioria das bibliotecas ter muitas dependências é que muitos desenvolvedores preferem construir coisas sobre dependências.
      Se a questão for o sistema de build, o Cargo não obriga o uso do Crates.io. Ele apenas facilita. Também é possível usar dependências baseadas em caminhos, como em CMake/Vcpkg e Conan, ou criar a biblioteca diretamente.
      Mesmo usando o Crates.io, se você não gosta de mudanças, basta fixar as versões. Ele só facilita obter a versão mais recente.
      Em Rust, é fácil construir software sobre software existente. Se você não gosta do software existente ou do ritmo em que ele muda, não culpe o Cargo; faça do jeito que preferir.
    • O verdadeiro problema de adicionar dependências em C++ é que é difícil adicioná-las de uma forma em que “todo mundo consiga compilar sem problemas”. Mesmo que funcione no meu computador, pode não funcionar no ambiente de potenciais usuários ou contribuidores, e isso vira um problema.
      As distribuições ainda compilam pacotes Rust a partir do código-fonte e vendorizam as dependências de crates no repositório. É mais doloroso porque há mais dependências e atualizações mais frequentes, mas isso é independente de bibliotecas compartilhadas.
  • Não é correto dizer que a API para descobrir o tamanho do terminal foi estável por 50 anos. Pelo que sei, o ioctl TIOCGWINSZ nunca foi padronizado, e há vários nomes diferentes entre Unix e BSD.
    A função tcgetwinsize() só entrou no POSIX em 2024, e todo esse tema tem uma história bastante lamentável https://news.ycombinator.com/item?id=42039401. Ou seja, isso já é verdade antes mesmo de chegar ao lado do Windows.

    • A impressão que tive do texto é menos que a implementação alternativa seja melhor e, portanto, simples, e mais que ela é simples porque só trata do próprio caso de uso.
      Às vezes isso é aceitável, mas pode piorar o software para pessoas com outros casos de uso ou para quem o executa em sistemas que o autor não entende e não usa.
      O verdadeiro valor de uma biblioteca está em lidar com o fato de que até problemas que parecem simples na verdade têm muita complexidade. Também não acho que 3 ou 4 dependências em uma biblioteca que roda em Windows/Linux seja tanta coisa assim. Num bom caso, quem escreveu a biblioteca tem mais experiência naquela área e consegue lidar até com partes desconhecidas que eu não conheço.
    • Embora não tenham sido padronizadas, essas chamadas não mudam. Especialmente as chamadas do Windows, que estão compiladas em inúmeros binários, então a estabilidade de ABI é garantida.
      O ioctl certamente tem problemas, mas as mudanças que geraram várias releases em terminal-size ou em suas dependências não têm relação com as constantes ioctl/TIOCGWINSZ nem com a struct winsize. Esse código não mudou.
    • Neste caso, o crate terminal-size simplesmente chama tcgetwinsize do Rustix, e o Rustix, por sua vez, chama tcgetwinsize da libc. Então, se você fizer a mesma coisa diretamente, dá para reduzir bastante as dependências, e o custo é basicamente o suporte a Windows.
      Se essa API foi estável por 50 ou por 25 anos é um detalhe. Essa dependência nem finge lidar com essa complexidade, e também é improvável que essa função mude ou seja removida num futuro próximo.
    • Terminais são um bom exemplo de algo que parece muito simples, mas virou uma grande dor de cabeça porque havia fabricantes demais no início e nenhum padrão do setor se consolidou. Nem fica claro se o mais próximo é VT100 ou VT102.
      A maior parte das coisas escreve diretamente ali, mas recursos como tamanho do terminal ou modo raw são uma bagunça e exigem coisas como ioctl. Sinceramente, não é nada bom.
      Só que as bibliotecas são ainda piores. Se você não quer linkar com ncurses, que Deus o ajude.
  • Recentemente, ressuscitei meu primeiro web app de startup, criado em 2006. Era um site de mídia social focado em compartilhamento de mídia e, para os padrões da época, usava uma stack LAMP bem simples. Era PHP 5 e MySQL 3.2, mas já tinha a maioria dos recursos de mídia social daquele período
    Como eu queria experimentar pessoalmente novas tecnologias de CI/CD, estou usando esse app para aprendizado, fazendo uma engenharia exagerada no processo de deploy. Eu poderia ter usado WordPress ou um app Hello World, mas isso é muito mais divertido
    Escrevi quase todo o PHP eu mesmo. Também criei minhas próprias bibliotecas para autenticação/autorização, templates, processamento de formulários etc., e usei apenas uma biblioteca PEAR para envio de e-mails. O frontend era HTML puro, quase sem JavaScript, e usava Flash para reprodução de mídia. Em 2006, era assim que a maioria das coisas era feita
    Levei só cerca de uma hora para colocar de novo em execução um app de 19 anos. Troquei o antigo driver PHP mysql por mysqli e ajustei o schema e algumas queries para o MySQL 8. Foi basicamente colocar crases em palavras que agora são reservadas e corrigir defaults de colunas que ficaram mais rígidos. A única coisa que não funcionou foi o Flash
    Por outro lado, no trabalho atual operamos dezenas de apps Spring Boot escritos em Java 8, e há listas de vulnerabilidades, página após página, vindas de dezenas de dependências. Quando se atualiza uma, é preciso subir várias outras bibliotecas em cascata, e isso vira um pesadelo por causa das dependências transitivas. Por isso, tratamos minimamente só as vulnerabilidades mais críticas, e não há um plano realista para atualizar tudo
    O engraçado é que o que o app PHP de 2006 fazia não é tão diferente do que esses apps Spring Boot atuais fazem. No fim, é tudo CRUD; só há muito mais enfeite e ferramental enterprise em volta

    • Fui convencido pela filosofia de bibliotecas grossas ao observar o mundo de Go e de Linux em C. A ideia é trazer uma biblioteca que resolva um domínio de problema e acrescentar a ela o que for necessário
      Não se puxa uma dependência para cada item de um checklist. Pode haver trabalho duplicado, mas o caminho de upgrade fica muito mais fácil
    • A maior parte das novas tecnologias de CI/CD virou padrão por causa da complexidade gerada por manter dependências de terceiros e ambientes de execução que mudam o tempo todo. Em aplicações LAMP antigas implantadas com scp, isso geralmente não é um problema
    • Concordo em parte, mas não quero gastar tempo reinventando a roda e colocando bugs nela
      Por exemplo, formatos padronizados e complexos de mensagens de comunicação, como FHIR ou HL7, não são algo cuja definição inteira você vá querer implementar por conta própria
      Funções criptográficas também costumam ser um tiro no próprio pé quando escritas do zero, e os problemas graves de segurança descobertos ao longo dos anos provaram isso
      Hoje é uma época em que queremos focar em resolver problemas de negócio, mais do que em como a solução é construída corretamente. Com a chegada da IA, isso ficou ainda mais importante, porque todo código parece ter sido colado às cegas
      Gastar tempo construindo tudo por conta própria pode ser vantajoso no longo prazo, mas antes é preciso sobreviver à competição. O concorrente talvez já tenha conquistado o mercado no início com código rápido e descartável
    • No meu trabalho há uma política de análise estática bem rígida, e ela deve ficar ainda mais rígida a partir de abril. Fiquei curioso se você já viu https://docs.openrewrite.org/ para atualizar dependências automaticamente
      Recentemente migramos de Java 8, Spring Boot 2 e Swagger para Java 17, Spring Boot 3.3 e OpenAPI 3, e foi bem painless
      Ainda precisamos subir mais algumas dependências diretas e transitivas, mas o maior obstáculo foi resolvido pela migração
    • O ponto importante aqui é que Rust praticamente não tem o problema de dependências transitivas. Se você atualiza uma crate e suas dependências públicas também sobem, e essa dependência está exposta na API, de modo que você precisa interagir com ela, então é claro que precisa atualizar junto
      Mas, a menos que você esteja linkando com uma biblioteca C, dependências transitivas privadas não importam em nada. Você pode ter na mesma árvore de dependências quantas versões incompatíveis por SemVer de uma crate quiser e, se necessário, também pode depender diretamente de várias versões
      Não é preciso fazer grandes upgrades em massa no estilo Java; basta subir aquela que tem vulnerabilidade. Pelo que sei, C# tem algo parecido, mas é um pouco mais verboso
  • Concordo 100%. NodeJS teve um grande impacto na minha carreira, mas o NPM me traumatizou mais do que uma infância bagunçada
    Basta imaginar um programador novo criando um app para mostrar aos amigos. Ele tenta adicionar uma nova dependência, ela não combina com outra, então decide atualizar tudo. No instante seguinte nada funciona e o Babel começa a gritar
    Ainda dá para pensar que, de algum modo, isso será resolvido, mas logo você encontra uma issue aberta no Git dizendo que literalmente nem o básico funciona. Por exemplo, no Expo há uma issue aberta dizendo que um novo projeto React Native padrão não compila no Android
    Metade das vezes parece que ninguém se importa e, nesses casos, a solução nem está no ecossistema Node, mas em algum lugar do ecossistema Android. É fita adesiva até o fundo. Mesmo assim, se um projeto de bilhões de dólares pode distribuir um template que não funciona, isso também me dá confiança para não sentir síndrome do impostor quando metade do meu projeto paralelo não roda

    • Isso é um problema do Node, não do Rust. As dependências não precisam “gostar” umas das outras. Você pode ter todas as versões ao mesmo tempo, e nada quebra
    • O fato de Node.js e npm terem saído de controle foi um alerta para mim cerca de 15 anos atrás
      Passei a ver o momento de trazer a primeira dependência como um fracasso pessoal. Porque, naquele instante, em vez de simplesmente carregar JS comum com uma tag de script, passou a ser necessário todo um empacotamento e organização só para usar um package.json
  • Foi algo que me surpreendeu ao sair do Go e entrar no ecossistema Rust. Um projeto Go maduro — por exemplo, o backend web de produção de alguma empresa — pode ter apenas 10 a 20 dependências, contando até as dependências transitivas.
    Como o texto diz, mesmo um projeto Rust pequeno tem grande chance de ter muito mais do que isso; se fizer trabalho assíncrono, é praticamente garantido.
    Não sei bem se isso é uma questão de cultura ou de recursos da linguagem. Por exemplo, em Go, interfaces são satisfeitas implicitamente, então não é preciso importar algo só para dizer que você implementa uma interface.

    • Um dos grandes motivos é que Go tem uma biblioteca padrão excelente e completa, enquanto Rust realmente não tem isso.
      Em Go, coisas como green threads, canais, expressões regulares, cliente HTTP, servidor HTTP, tempo, flags de linha de comando, logger e leitura/escrita de GIFs animados estão na linguagem ou na biblioteca padrão; em Rust, exigem dependências.
      Eu não gosto tanto assim da linguagem Go em si, mas em ferramentas e biblioteca padrão ela está na dianteira, e é claramente a melhor entre as que já usei.
    • A diferença entre Rust e Go está especialmente na biblioteca padrão. A biblioteca padrão de Rust é intencionalmente pequena.
    • A vasta biblioteca padrão de Go ajuda muito a reduzir a quantidade de dependências.
  • Concordo bastante com a tese. Permitir que as abstrações de outra biblioteca sejam expostas para fora da sua própria biblioteca tem muitos custos ocultos. Não estou dizendo que nunca se deva fazer isso, mas, no momento da decisão, é preciso ponderar bem os custos futuros.
    Se o pacote que você está encapsulando muda o design com frequência ou muda seus objetivos, isso gera instabilidade. Funcionalidades que originalmente eram a forma mais simples de garantia em engenharia de software desaparecem ou são fragmentadas.
    Isso também dificulta a participação no ecossistema de mantenedores que talvez não sejam contribuidores open source de carreira, mas conhecem bem um domínio específico. Quando “criei um jeito limpo de fazer X” vira semanas de debate, negociação e política porque “talvez as pessoas gostem de Z, então X precisa se encaixar debaixo de Y”, todo mundo perde tempo.
    Uma coisa que aprendi na vida é que o mais simples é o que sobrevive por mais tempo. Deveríamos elogiar com mais frequência os miniliths e os monólitos. Isso nem é um problema só de Rust; já vi em várias linguagens. Comunidades open source muitas vezes empurram com força pacotes de tamanho atômico, e frequentemente me pergunto se o objetivo não é mais fincar bandeira ou transferir posse do que beneficiar a comunidade real de usuários.

  • Em 2025, cheguei meio por acaso à visão de que é mais rápido pedir ao ChatGPT ou ao Cursor para criar implementações sem dependências de funções comuns, e venho concordando cada vez mais com isso. Especialmente depois de passar pelo inferno de dependências de uma grande aplicação React.
    Também é preciso pensar nas dependências de SaaS ou serviços de terceiros. Muitas delas são padrões comuns e problemas já resolvidos, que um LLM consegue replicar rapidamente.

    • Hoje é muito mais provável eu começar com uma pequena função utilitária interna implementada por IA antes de adicionar uma biblioteca.
      Para problemas de escopo limitado, funciona bem, e consigo fazer a IA criar uma implementação mais completa e robusta do que eu escreveria sozinho.
      Mesmo que depois eu decida adicionar uma biblioteca, isso cria uma encapsulação natural. Basta mudar a implementação da minha função para usar a biblioteca, sem necessariamente mexer em todos os pontos onde ela é usada. Quando chegar a hora, também fica mais fácil testar várias bibliotecas.
  • Ironicamente, isso é bem interessante. Armin é o autor original do framework web Python Flask. Na mesma época, havia também uma biblioteca muito parecida chamada Bottle.
    As funcionalidades eram quase iguais, mas Flask ficou muito popular, enquanto eu sempre preferi Bottle. Por ser um arquivo único e não ter dependências, era muito fácil simplesmente copiá-lo para um projeto.
    Também era fácil modificá-lo e, no fim, cheguei ao ponto de entender o todo. Para servidor e websockets, eu anexava o Gevent, mas com esse método dava para criar projetos bastante pesados.
    Até hoje tenho uma forte vontade de usar Bottle em projetos web pequenos. O problema é que ele não acompanhou muitas das práticas modernas que Python adotou ao longo dos anos, então agora parece um pouco datado.

  • Criar por conta própria exige competência de engenharia. Se você só tem engenheiros acostumados a puxar bibliotecas o tempo todo de ecossistemas como NPM ou PyPI, eles terão dificuldade para desenvolver internamente soluções para muitos problemas. Especialmente se a solução precisar durar bastante e ter a flexibilidade necessária.
    “Não programar a si mesmo para dentro de um beco sem saída” exige muita prática.
    Também vi com frequência casos em que é fácil fazer melhor do que uma biblioteca existente. Em um projeto, implementei um parser para uma variante de Markdown com metadados no topo do arquivo. Usei uma gramática pequena e acabei com um parser em menos de uma tela de código.
    Só que a biblioteca de frontend que parseava o mesmo arquivo era muito pior do que eu esperava. Ela quebrava quando o identificador de metadados tinha hífen; descobri que usava diretamente o identificador de metadados como nome de membro de objeto. Ou seja, em vez de usar um objeto JSON simples, limitava artificialmente a escolha dos nomes e quebrava quando havia hífen, como em something-something.
    No fim, meu parser foi descartado, com a justificativa de que as pessoas teriam que “mantê-lo”. Mas ele simplesmente funcionava e podia se adaptar facilmente a mudanças na gramática. Para quem soubesse um pouco sobre parsers, não havia nada difícil de entender.
    É difícil de acreditar, mas acho que, no time, ninguém além de mim tinha escrito um parser com uma biblioteca geradora de parsers. Há muitos outros exemplos como esse.

  • Se você usa só uma função, mas acaba compilando centenas, isso deveria acender uma luz de alerta. Cerca de um ano atrás, trabalhei em um projeto para atualizar dependências de terceiros
    Uma delas era uma biblioteca matemática rica, com todo tipo de função matemática. Investigando um pouco, descobrimos que usávamos apenas um único método: encontrar a mediana de uma lista
    Apontei a página da Wikipedia para o engenheiro responsável e pedi que removesse a dependência e escrevesse um único método que realizasse aquela operação matemática
    Mas o problema real não é usar dependências de terceiros em si; é a necessidade de um conceito para trazer apenas uma fatia estreita da biblioteca. Se você precisa só de uma pequena parte de uma biblioteca enorme, por que trazer tudo? Já ouvi alguém propor “microframeworks” como abordagem para isso

    • Em Rust, bibliotecas podem definir features que podem ser ativadas ou desativadas condicionalmente, então há uma forma embutida de controlar o que de fato é incluído. Tokio é um bom exemplo: talvez surpreenda saber que as dependências diretas necessárias para o Tokio somam apenas duas. Todo o resto é opcional https://github.com/tokio-rs/tokio/blob/ee19b0ed7371b069112b9...
      Infelizmente, não parece que as pessoas examinem com cuidado o conjunto de features padrão usadas pelas dependências e as reduzam ativamente. A sintaxe para trazer features adicionais é simples, mas, para remover features opcionais ativadas por padrão, é preciso especificar no-default-features e ainda adicionar de volta, uma a uma, as features padrão que você quer manter, o que é mais trabalhoso
      Pior ainda: para que uma biblioteca permita cortar features desnecessárias de suas próprias dependências, ela precisa criar suas próprias features e mapeá-las para as features de cada dependência. Por exemplo, se houver 5 dependências e cada uma tiver 1 dependência obrigatória e 4 opcionais, para dar ao usuário controle total sobre features transitivas, você teria de criar 20 features de mapeamento na sua própria biblioteca. Isso nem inclui as features que você criaria no seu próprio código por consideração aos usuários a jusante
      Cada vez mais, vejo que a usabilidade em torno de features, especialmente para reduzir inchaço desnecessário, é tão ruim que acaba sendo um catalisador dos problemas de tempo de compilação em Rust. Como esse assunto não parece ser amplamente discutido quando surge, talvez eu deva escrever um post no blog com minha opinião forte, para poder apontar para ele se o estado atual continuar
    • Se fizer isso, acontece algo como o leftpad no ecossistema JS
      Se o sistema de confiança for bom, por um lado acho que não é um problema
      Também gosto até certo ponto de uma abordagem como a do Shadcn: se você gosta de um componente, copia para a sua própria biblioteca. Mas, se surgir uma vulnerabilidade, não dá para saber se fui afetado
      Em alguns códigos isso é aceitável, mas em outros você realmente precisa depender de algo visto pelo maior número possível de olhos
    • Para quem não sabe, em pseudocódigo seria mais ou menos isto
      Median(list) { let len = length(list) if len % 2 == 0 { let x = floor(len/2) return (list[x] + list[x+1]) / 2 } return list[len/2] }
      Presumindo, porém, que a lista já esteja ordenada. Aqui resisti à tentação de chamar is_odd