Rust valeu a pena?
(medium.com/@jsoverson)- Depois de migrar de JavaScript para Rust para focar em WebAssembly, o autor avaliou o valor real de Rust ao longo de 3 anos criando o Wick, implantações em produção, um ebook e cerca de 100 pacotes no crates.io
- O borrow checker, o sistema de tipos rico, padrões funcionais e a ausência de
nullevitam muitos erros em tempo de compilação e permitem manter uma base de código grande com menos testes - Clippy e Cargo workspace são poderosos, mas lacunas em ferramentas e no ecossistema, como configuração global de lint e publicação de workspaces, acabam gerando custo operacional
- Async, refatoração e o gerenciamento de generic·lifetime·trait constraints continuam sendo áreas com mais atrito do que em JavaScript ou Go
- Rust é robusta e versátil, mas os custos de contratação, aprendizado, iteração rápida e rastreamento de problemas são altos, então faz mais sentido quando o escopo está bem definido ou quando é possível arcar com o custo inicial
WebAssembly levou à escolha de Rust
- Há alguns anos, o autor deixou de lado o trabalho que fazia para se dedicar 100% a WebAssembly, e naquela época Rust oferecia o melhor suporte para compilação em WebAssembly
- Os runtimes de WebAssembly mais completos também eram baseados em Rust, então, entre as opções disponíveis, Rust era a alternativa mais realista
- Depois disso, ele criou o Wick, um framework e runtime de aplicações que usa WebAssembly como sistema central de módulos
- Ao longo de 3 anos, acumulou experiência com Rust passando por várias implantações em produção, um ebook e o envio de cerca de 100 pacotes para o crates.io
Mantendo mais código com menos testes
- Em Rust, ele começou escrevendo testes como faria em linguagens comuns, mas percebeu que estava criando testes que não poderiam falhar se o código já compilasse
- Ao evitar blocos
unsafe {}e métodos propensos a panic, como.unwrap(), muitos problemas acabam sendo evitados por padrão - O borrow checker, o sistema de tipos rico, os padrões e bibliotecas funcionais, e a ausência de valores
nullreduzem o esforço necessário com testes - No projeto Wick, ele manteve mais de 70.000 linhas de código com muito menos testes do que precisaria em outras linguagens
- Quando testes são necessários, o harness de testes integrados do Rust facilita adicioná-los ao lado do código
Rust também mudou hábitos de programação em outras linguagens
- O compilador de Rust continuava reclamando de códigos que em outras linguagens ele considerava normais, e esse processo acabou mudando seus hábitos de programação
- Agora, mesmo em outras linguagens, ele se incomoda quando a ordem das linhas de código parece estranha ou quando um valor de retorno não é verificado
- Também passou a ter uma aversão muito maior a erros em tempo de execução
- O rigor de Rust é incômodo, mas, depois que se acostuma com a proteção oferecida pelo compilador, fica difícil voltar para outras linguagens
Clippy é útil além de ser um linter
- O Clippy é o linter de Rust, mas funciona mais como uma ferramenta auxiliar amigável que sugere código alternativo do que como um simples verificador
- A biblioteca padrão de Rust é muito grande, e as funcionalidades estão espalhadas entre tipos, traits, macros e funções, o que dificulta encontrar a API necessária
- Várias regras detectam padrões comuns que podem ser substituídos de forma melhor por métodos ou tipos da biblioteca padrão
- Exemplo: manual_is_ascii_check
- Há centenas de regras cobrindo desempenho, legibilidade e indireções desnecessárias, e muitas também oferecem código alternativo quando possível
- A configuração global de lint por projeto parecia que seria viabilizada por uma issue do Cargo, mas até lá o Wick precisou atualizar automaticamente, via script, configurações inline de lint em dezenas de crates
O ecossistema tem lacunas que precisam ser aceitas
- O problema da configuração global do Clippy é apenas um exemplo das lacunas do ecossistema que aparecem com frequência nas ferramentas e bibliotecas de Rust
- As issues relacionadas já foram fechadas, mas ficaram abertas por anos e demoraram muito para serem resolvidas
- Rust atrai novos usuários há muito tempo, a ponto de ser eleita repetidamente como “a linguagem mais querida”, mas esse fluxo não se traduziu diretamente em melhorias drásticas em bibliotecas e ferramentas
- Era comum surgirem forks pontuais para atender casos de uso específicos, e no Wick também houve tentativas de enviar PRs que esbarraram em situações parecidas
- Entre as possíveis razões estão a pressão por manter APIs estáveis e a granularidade do sistema de tipos
- Para quem mantém bibliotecas, até mudanças pequenas podem acabar exigindo mudança de versão principal, o que dificulta aceitá-las
- Também é grande o peso de escrever código Rust que satisfaça as necessidades de todo mundo
Cargo, crates.io e o atrito na publicação de workspaces
- A estrutura do repositório do Wick foi criada com base em projetos populares e, no começo, parecia razoável, mas os problemas apareceram na etapa de publicação
- Com Cargo, é fácil compilar, testar e usar crates em tamanho modular, mas publicar no crates.io é outra história
- No crates.io, todos os crates referenciados precisam estar publicados individualmente para que um pacote possa ser publicado
- Faz sentido impedir a publicação de crates que dependam de pacotes que existam apenas no sistema de arquivos local
- Mas, em uma estrutura natural de projeto grande dividido em pequenos módulos internos, não é possível publicar incluindo sub-crates que existam apenas dentro do crate pai
- Houve uma correção informando que crates com dependências locais de desenvolvimento ainda podem ser publicados se
versionnão estiver incluído noCargo.toml - O suporte a workspaces do Cargo em si é excelente, e a experiência de gerenciar projetos grandes é melhor do que na maioria das linguagens
- Porém, o workspace não resolve o problema de publicação, e, mesmo havendo várias formas de configuração, é difícil encontrar uma “resposta certa” que publique com facilidade
- A existência de muitos crates utilitários relacionados a cargo workspace publish já mostra o problema
- Ao publicar o Wick, era comum gastar mais de 1 hora combinando tarefas manuais repetitivas com ferramentas que funcionavam só parcialmente
Async é uma das maiores fontes de atrito
- O async em Rust parece um recurso adicionado depois que a linguagem já estava pronta, e, no uso real, frequentemente também passa essa sensação de remendo tardio
- É difícil entender e resolver os erros, e, ao procurar soluções, ainda é preciso filtrar por múltiplos runtimes e suas abordagens próprias de async
- Algumas bibliotecas async talvez nem possam ser usadas fora de um runtime async específico
- Para alguém com 20 anos de JavaScript e experiência com Go, o async de Rust foi a maior fonte de frustração e atrito
- Não é um problema impossível de superar, mas é preciso estar sempre preparado para o surgimento de problemas com async
- Em outras linguagens, o async costuma funcionar de maneira tão natural que quase nem aparece
Refatorar pode virar um trabalho pesado
- O sistema de tipos rico de Rust é tanto vantagem quanto desvantagem
- Pensar com os tipos de Rust é bom, mas gerenciar os tipos de Rust pode virar um pesadelo
- Assinaturas de dados e funções podem incluir generic types, generic lifetimes e trait constraints
- As próprias constraints também podem ter generic types e lifetimes, então às vezes há mais constraints de tipo do que código real
- Exemplo: rxRust observable.rs
- É preciso definir generics em cada
impl, então isso já é trabalhoso na primeira escrita e, durante refatorações, pequenas mudanças podem se transformar em cascatas de ajustes- Exemplo: Wasmtime Cranelift iter.rs
- Quando a mesma lista de constraints ou generics precisa ser repetida em vários lugares, não há um recurso da linguagem ou da ferramenta para criar alias disso ou referenciar uma definição central, então o peso da duplicação permanece
Veredito final: poderosa, mas cara
- Rust é versátil a ponto de permitir escrever código de nível de sistema, apps CLI, servidores web e clientes web na mesma linguagem
- Com WebAssembly, é possível executar o mesmo binário de um LLM no navegador e na linha de comando
- Programas em Rust podem ser extremamente robustos, e, quando se sente na prática os problemas que Rust evita, fica difícil voltar para outras linguagens
- Quando voltou temporariamente para Go, a velocidade de desenvolvimento voltou a parecer atraente, mas, depois de enfrentar um panic em tempo de execução, essa vantagem perdeu força
- Rust tem desvantagens claras
- É difícil contratar
- O aprendizado é lento
- É rígida demais para iteração rápida
- É especialmente difícil rastrear problemas de memória e desempenho, principalmente em código async
- Nem todas as bibliotecas são boas o suficiente para código seguro
- As ferramentas de desenvolvimento ainda têm bastante espaço para melhorar
- Foi possível fazer coisas impressionantes com um time pequeno, mas também houve grandes obstáculos, e, como havia razões técnicas claras para Rust ser mais adequada ao Wick, o autor considera que ainda é cedo para dizer se Rust realmente valeu a pena nesse caso
- Se for necessário iterar rapidamente, é muito provável que Rust não seja a escolha ideal
- Se o escopo for conhecido ou se houver condições de absorver um custo inicial maior, Rust merece ser considerada seriamente
- À medida que a perspectiva de WebAssembly ganha força a cada mês, a possibilidade de reutilizar em muitos lugares um software robusto escrito uma vez só parece cada vez mais próxima da realidade
1 comentários
Opiniões no Hacker News
Usei bastante Rust, mas mesmo depois de alguns anos ainda sinto que a produtividade é baixa
Hoje em dia uso bastante Zig e sinto que sou umas 10 vezes mais produtivo, porque posso me concentrar só no código que quero escrever e não preciso ficar pensando em quais ferramentas ou bibliotecas usar
Sei que Rust oferece segurança de memória e que isso é importante, mas a usabilidade é realmente ruim. Toda vez que uso Rust, sinto que estou sendo limitado, e como sempre preciso procurar bibliotecas ou pesquisar como fazer as coisas, não consigo simplesmente “digitar código”
O sistema de tipos também pode crescer de forma descontrolada, e muitas vezes é difícil saber quais métodos podem de fato ser chamados em uma determinada struct. Rust é uma ferramenta excelente e resolve muitos problemas, mas não acho que seja uma boa linguagem de uso geral
Falando como alguém que usou Python por quase 20 anos, hoje trabalho em Rust tão rápido quanto em Python
Para mim, Rust parece ter errado completamente o ponto de equilíbrio adequado. É exigente demais com detalhes de baixo nível para escrever aplicações de alto nível, e complexa demais para embedded ou sistemas operacionais
Para o primeiro caso, eu escolheria C++, Java, Haskell, OCaml, ou até Go com um pouco de C; para o segundo, C usado quase como macro assembly é muito mais adequado
Ainda tenho a sensação de que a ideia original de Graydon Hoare — algo mais próximo de OCaml/SML, com tipos lineares, garbage collection, alocação na stack, green threads e CPS — teria se tornado uma linguagem muito melhor
Em C, um pequeno erro frequentemente leva a comportamento indefinido e dores de cabeça; em Rust isso não existe, e isso muda completamente o jogo
Também queria entender se, em Zig, você quer dizer que não precisa procurar bibliotecas nem descobrir como fazer as coisas
Rust obriga você a decidir antecipadamente para onde cada bit e byte vai, em qual thread será usado e sob que forma de mutação será tratado. A menos que seja no nível de parsers ou microcontroladores, esse processo parece tedioso
Gosto de primeiro fazer algo funcionar e depois decidir a melhor estrutura de API, mas Rust entra em conflito com esse processo
Embora o sistema de tipos de Rust seja mais poderoso, com Swift também dá para obter 90% do desempenho, e o fluxo é muito mais natural
A falta de namespaces no crates.io talvez seja o maior ponto de crítica
Qualquer pessoa pode reservar nomes globais e genéricos de pacotes, e, a menos que se evite o repositório crates.io, a maioria precisa aceitar isso. Só que alguns desses pacotes com nomes genéricos reservados nem são, na prática, os melhores para usar
Talvez tenha sido uma reação à notação DNS reversa ao estilo Java, que é verbosa e irritante, mas um meio-termo bom teria sido prefixar o nome do pacote com um namespace de usuário/grupo, como no GitHub
Enviei essa análise para a equipe do crates.io e também apontei que existe uma política contra automação, mas eles responderam que isso não era prova suficiente de ocupação de nomes
O problema do crates.io é que, mesmo quando há uma política clara, ela não é aplicada. Por isso, nomes curtos e fáceis de lembrar para crates já foram todos tomados, e não há como recuperá-los
NPM, PyPI, RubyGems, Hex do Elixir, Cabal do Haskell etc.; por volta de 2014–2015, quando Rust surgiu, não me lembro bem de gerenciadores de pacotes que não fossem Java e que não tivessem um namespace global único
Alguns tentaram corrigir isso depois, mas na época era simplesmente assim que os gerenciadores de pacotes funcionavam
Os sistemas de gerenciamento de dependências piores que surgiram depois em outras linguagens aprenderam muito pouco com os exemplos anteriores
Também tem a vantagem de eliminar a necessidade de um banco de dados global de pacotes no nível da linguagem. Se você publica um pacote em example.com/your-thing, ele já está lançado
Claro, se quiser, caches e motores de busca podem ser oferecidos separadamente
É algo como: http-server é ruim, então não use, use MuffinTop — e você simplesmente precisa saber disso
A ideia de um nome de pacote oficial é interessante, mas, se com o tempo o código por trás do alias mudar, na prática isso pode ficar confuso
No fim, parece que isso continuará sendo parte do processo de se tornar especialista de domínio em qualquer ecossistema
Se você criar
.cargo/config.tomlna raiz do workspace, ele se aplica a todos os crates, então dá para configurar lints globais do ClippyDentro do arquivo, basta colocar algo como
rustflags = ["-Wclippy::lint_name_to_warn", "-Dclippy::lint_name_to_deny"]sob[build]Mas
rustflagsnão é aditivo; ele sobrescreve. Portanto, se houver outra fonte, como a variável de ambienteRUSTFLAGS, ela sobrescreverá essa configuraçãolib.rsoumain.rs. É simplesEstou aprendendo Rust porque parece claro que ele vai se tornar profissionalmente importante. Quero muito gostar da linguagem e vejo suas vantagens, mas ela está entre as mais desagradáveis que já usei até agora
Continuei esperando que, com mais proficiência, a antipatia desaparecesse, mas, quanto mais avanço na curva de aprendizado, não é que eu crie muito carinho por ela
Tudo bem. Provavelmente não será a única linguagem que eu domino e ainda assim não gosto. Só que há tanta gente dizendo que ama Rust que achei que eu também fosse curtir
O compilador Rust também parece ter melhorado para aceitar uma gama mais ampla de casos válidos
A chave para entender o borrow checker foi compreender o modelo de memória subjacente. O modelo de memória do Rust é igual ao do C, exceto por extensões para abstrações como genéricos
As regras do borrow checker parecem arbitrárias no começo, mas estão profundamente conectadas a esse modelo de memória. O verdadeiro valor aparece quando você esbarra no borrow checker sem querer, porque isso indica um bug criado por uma falta de atenção
O assustador é que, em linguagens como C ou C++, esse código poderia simplesmente ser aceito e seguir adiante
O sistema de tipos rigoroso e o borrow checker do Rust empurram você suavemente para estruturar o código corretamente, e tenho certeza de que melhoraram meu design de código em todas as linguagens que uso
É um recurso bem básico de usabilidade para programadores, compartilhado pela maioria das linguagens populares, e até C++ tem argumentos padrão há muito tempo
Sou um ex-programador C/C++ que chamava pthread quase toda semana durante 10 anos, e hoje uso Rust assíncrono em todo lugar
Não sei por que async recebe tanto ódio. Na minha opinião, todo mundo deveria usar async para tudo. Incluindo até tarefas “simples” que, por fora, parecem single-thread
Se o caso de uso do autor fosse Wasm, certamente ele teria outra perspectiva
Trabalhos que usam buffers grandes ou os reutilizam para evitar custo de alocação muitas vezes também se beneficiam de pools de threads à moda antiga. Dá para resolver isso até certo ponto com bump allocators ou allocators com shards, mas, em loops apertados vetorizáveis e limitados por CPU, pools de threads funcionam melhor
Async é uma boa ferramenta, mas não existe apenas o contexto em que ela é ideal
Go, C# e TypeScript não têm essa barreira
Por exemplo, sem o decorator importado do Tokio, parece que nem dá para usar
awaitdentro da funçãomainEnginedo rhai, não sãoSend, e estou tentando usá-los dentro de uma closure assíncronaO GPT-4 sugeriu criar um
tokio Runtimedentro de uma thread e usarblock_on(). Vou tentar amanhã. Este é meu primeiro projeto Rust de verdadeTambém fico curioso se “agora uso Rust assíncrono em todo lugar” significa “uso Tokio em todo lugar”
Programar em Rust realmente não se parece com uma relação abusiva. O compilador tenta ajudar o máximo possível, e em especial as mensagens de erro do rustc estão entre as melhores do mundo
O que mais se aproxima de uma relação abusiva não é o compilador Rust, mas o sistema operacional. O hardware também precisa executar assembly corretamente, então, visto desse jeito, também poderia ser chamado de relação abusiva
As mensagens de erro do Rust são únicas, e não há compilador que chegue perto
Além disso, usei LaTeX recentemente e as mensagens de erro eram horríveis. Foi um pesadelo ficar olhando os erros para descobrir o que estava errado
Futurenão é maisSendeSyncO console inteiro fica coberto de erros, e o erro de sintaxe real fica enterrado em algum lugar no meio
Minha principal reclamação é que ainda não entendo completamente lifetimes, e o compilador nem sempre consegue ajudar. Dá para entender, porque o compilador julga de forma conservadora
Em testes em C++, costuma-se dizer que “se compila, provavelmente está certo”
Se você acha que, como Rust lida com muitos erros, casos de teste comuns se tornam sem sentido, vejo isso como sinal de que, em outras linguagens, você não estava testando o que era correto
O que deve ser testado não são os problemas da linguagem em si, mas a lógica de negócio
Se, ao olhar para um código de teste, você sente “em JavaScript eu testaria, mas em Rust não preciso”, então basta apagar esse teste
Não existe essa distinção entre “lógica de negócio versus problema da linguagem”. A linguagem é a base sobre a qual a lógica de negócio fica
Se você não testa modos de falha, não sei qual é o sentido dos testes
Ao contrário da maioria das linguagens conhecidas, C++ tem IFNDR, chamado em tom de piada de falso positivo para a pergunta “isto é um programa C++?”
Um compilador C++ em conformidade com o padrão é proibido, em alguns casos, de avisar que suspeita que o código escrito não faz sentido; ele deve simplesmente continuar e emitir alguma coisa
Pode ser um executável que funciona, ou um executável que causa uma catástrofe toda sexta-feira. Não há como saber
O padrão ISO até identifica esses casos, mas é vago demais para delimitar exatamente o que entra nisso, e meu palpite é que hoje a maior parte dos softwares C++ não triviais é, na prática, IFNDR. É melhor simplesmente rejeitar a linguagem inteira
Rust parece ter finalmente quebrado a ideia de que o programador precisa controlar e ter consciência total de tudo o que o compilador faz
Na verdade, isso já não era verdade havia décadas, e compiladores eram quase mágicos. Com empréstimos, Rust reverteu bastante esse fluxo, fazendo as pessoas se sentirem confortáveis com a ideia de que o compilador sabe melhor do que elas
Gostaria que isso fosse ainda mais longe. Se não for algo necessário para o algoritmo, não deveríamos precisar iterar explicitamente uma coleção do começo ao fim. Várias operações deveriam ser paralelizadas implicitamente. Seria bom ter um Bash ao estilo Rust
Rust é bastante transparente sobre o que faz e é muito conservador em relação a mágica do compilador. A linguagem não faz alocação no heap, não faz contagem de referências e não tem conversões implícitas de tipos numéricos
Tipos que não foram declarados como copiáveis implicitamente não são copiados, e mesmo isso só é legal para tipos que podem ser copiados por um simples
memcpyrasoRust usa abstrações de custo zero por toda parte, então é previsível em que código algo será compilado e, em geral, é simples. O layout básico dos tipos padrão também é bem conhecido, então dá para saber que percorrer um
Vecserá compilado para um loop que incrementa um ponteiro, e não há paralelismo implícitoDescrever empréstimos como “o compilador saber melhor que o programador” é estranho. Empréstimos são parecidos com verificação de tipos. Se você declara que certo tipo é temporário e tenta usá-lo como se tivesse vida longa, dá erro
É como uma função declarada para retornar uma struct
Fooretornar umBar: dá erro. O compilador “sabe melhor” simplesmente porque você escreveu um bugEmpréstimos também compilam para uso direto de ponteiros sem garbage collection e, em structs e funções com ABI C, são garantidamente idênticos a ponteiros C em sentido literal. Se você acha que sabe melhor que o compilador, também pode contornar lifetimes com
unsafeiter()Mas não concordo que isso deva ser implícito
Em certos aspectos, Rust dá mais controle ao programador do que C. Por exemplo, Rust oferece suporte padrão a assembly inline, enquanto o assembly inline em C depende de extensões específicas de fornecedor
Mas os padrões convenientes são muito diferentes. Em Rust, casts de tipo inseguros exigem muitos procedimentos e cautela, e é preciso seguir mais regras do que em C
Em especial, as referências de Rust se comportam praticamente todas como
restrict, e é muito fácil estragar isso ao fazer um castunsafede um ponteiro bruto para uma referência segura. Por isso, se houver escolha, cria-se um forte incentivo para não escrever esse tipo de códigoPelo contrário, quero controle mais explícito e gostaria de reduzir esse ônus com um sistema de tipos mais expressivo. Idealmente, o sistema de tipos de Rust seria como uma variante de Prolog
Aprender quando investir em restrições de tipo e quando não investir é uma lição importante
Não é um problema exclusivo de Rust, embora a forma de expressá-lo possa ser um pouco diferente
Já lidei com C++ excessivamente tipado e Java excessivamente abstraído e tipado, e ambos têm o mesmo tipo de problema de refatoração
Por outro lado, também vi muito Go com tipos insuficientes e pouca documentação, em que certos valores ficam espalhados por toda parte, virando minas terrestres em tempo de execução e tornando a refatoração realmente muito difícil
A sensação inicial de progresso vem mais rápido, mas em geral você acaba entregando bugs aos usuários
Não há uma resposta mágica para esse trade-off. Rust oferece uma gama bastante ampla de escolhas nesse eixo
A frase “Rust grita com você o dia inteiro, todos os dias, por coisas que, na vida anterior, você teria considerado totalmente normais” é algo que um bom compilador C também faz de forma parecida se você ativar todas as flags
Gosto de linguagens e compiladores que permitem desligar seletivamente esses gritos e escrever código ruim de propósito. Um código ruim que funciona e que dá para escrever rápido muitas vezes é melhor do que um código perfeito que leva uma eternidade
Você pode criar uma prova de conceito ruim, mas funcional, e depois corrigi-la para ficar menos ruim
O ponto de que “atrai bem novos usuários, mas as bibliotecas ou ferramentas não melhoram dramaticamente, e só surgem forks pontuais para tratar casos de uso específicos” não tem relação com idade
Atrair desenvolvedores centrais é difícil, e é preciso muito esforço para tornar isso atraente. Além disso, as convenções culturais são definidas pelos adotantes iniciais, e a ausência de convenções muitas vezes pode ser tão prejudicial quanto convenções ruins
Olhando para Python, a abordagem frouxa em relação aos ambientes de desenvolvimento e execução resultou em umas 50 formas concorrentes de desenvolver ou executar programas Python
O repositório de pacotes mais usado, o PyPI, foi uma bagunça por anos; pouca gente constrói em cima de pacotes existentes, os nomes parecem saídos de um gerador de palavras aleatórias, há muito código malicioso no ecossistema e nem dá para pesquisar pacotes pela linha de comando
Isso não é culpa da linguagem, mas da comunidade e da equipe principal, que ficaram como espectadoras. A cultura é mais importante do que a tecnologia no centro dela
Não quero criticar apenas Python; é só que conheço melhor esses problemas. C existe há meio século, mas a comunidade dele também não conseguiu organizar adequadamente nem metade das soluções que linguagens mais modernas já providenciaram
É aquela situação em que o compilador grita para você corrigir, mas você só quer testar uma ideia antes de deixá-la perfeitamente polida