O núcleo do Rust
(jyn.dev)- Rust é uma linguagem em que vários conceitos estão profundamente entrelaçados, e mesmo para entender programas básicos é preciso aprender muitos elementos ao mesmo tempo
- Funções, genéricos, enums, pattern matching, traits, referências, ownership,
Send/Sync,Iteratoretc. são todos elementos centrais projetados para interagir entre si - Em comparação com JavaScript, no JS é possível escrever código conhecendo apenas alguns conceitos, mas no Rust só é possível escrever código realmente significativo ao entender o contexto da linguagem como um todo
- Essa complexidade do Rust aumenta a barreira de aprendizado, mas ao mesmo tempo oferece segurança e consistência, além de influenciar fortemente a forma de projetar código
- Essa composição linguística é o que torna Rust especial, e a visão de um “Rust menor” nos faz revisitar uma filosofia de linguagem cuidadosamente integrada
A dificuldade de aprender Rust
- Apesar da alta barreira de entrada, muitas pessoas têm contribuído para melhorar documentação, APIs e diagnósticos
- Entre os conceitos básicos estão funções como objetos de primeira classe, enums, pattern matching, genéricos, traits, referências, borrow checker, segurança de concorrência e iteradores
- Esses conceitos dependem uns dos outros e estão entrelaçados, o que dificulta aprendê-los separadamente, e a biblioteca padrão também faz amplo uso desses recursos
- Para entender até mesmo cerca de 20 linhas de código Rust, é preciso compreender ao mesmo tempo vários elementos, como paradigma funcional,
Resulte tratamento de erros, tipos genéricos, enums e iteradores
Comparação entre Rust e JavaScript
- Ao escrever o mesmo programa de detecção de mudanças em arquivos em Rust e JS, no Rust diversos conceitos da linguagem ficam entrelaçados
- No JS, basicamente basta entender funções e tratamento de null para escrever código funcional
- Isso não significa simplesmente que Rust é mais difícil, mas mostra que Rust foi projetado para exigir uma compreensão estrutural da linguagem como um todo
O design interconectado do Rust
- O núcleo do Rust é a combinação de recursos projetados de forma orgânica
- Enums são incômodos sem pattern matching, e pattern matching também é limitado sem enums
ResulteIteratornão podem ser implementados sem genéricos- Os conceitos de
Send/Synce as restrições deprintlnsó podem ser expressos com segurança graças às traits - O borrow checker garante a segurança de
Send/Syncpor meio da análise de captura de closures
- Essa interconexão faz de Rust não apenas um conjunto de recursos, mas um sistema de linguagem integrado
A visão de um Rust menor
- Em 2019, without.boats mencionou “Smaller Rust” ao discutir a possibilidade de um Rust menor e mais refinado
- Hoje Rust cresceu muito mais, mas a ideia de um Rust menor nos relembra a essência de um design de linguagem cuidadosamente encaixado
- O charme do Rust está no fato de que seus elementos linguísticos são independentes entre si e, ao mesmo tempo, quando combinados, oferecem forte expressividade e segurança
Conclusão
- Rust é difícil de aprender, mas a consistência e integração dos conceitos entrelaçados funcionam como uma grande força
- Graças a essa estrutura, Rust leva o desenvolvedor não apenas a escrever código, mas a adotar uma forma de pensar que considera segurança e desempenho ao mesmo tempo
- A essência do Rust está em um “núcleo de linguagem pequeno e sofisticado”, e essa continua sendo uma filosofia importante mesmo no Rust expandido de hoje
1 comentários
Comentários do Hacker News
fs.watchdeixa explícito que é obrigatório verificar sefilenamepode sernullno callback. Em Rust, esse fato seria refletido no sistema de tipos e forçaria o tratamento, mas em JS é fácil escrever código de qualquer jeito. Documentação relacionadanullé obrigatória. Então acho que esse é um bom exemplo de como TS é uma etapa relativamente leve que aproxima JS um pouco mais da correção do lado do Rustfor path in pathsdeveria serfor (const path of paths). Em JS, sem os parênteses isso já dá erro imediatamente, mas a diferença entreineofé queinitera sobre os índices do iterável, não sobre os valores, então na prática o índice virastringe acaba entrando como primeiro argumento defs.watch. E nem o TypeScript necessariamente pegaria esse errokind. Emconsole.log("${kind} ${filename}"), o correto seriaeventType(umastring), nãokindprintlnde Rust só consegue imprimir tipos que implementam as traitsDisplayouDebug. Por isso,Pathnão pode ser impresso diretamente. Nem todos os sistemas operacionais armazenam caminhos compatíveis com UTF-8, e todos os tipos de string de Rust são UTF-8. Ou seja, imprimirPathpode envolver perda de informação.Pathretorna, via métododisplay, um tipo que implementaDisplay. Rust incorporou isso ao sistema de tipos, mas em JS/TS é difícil explicitar que internamentestringé UTF-16, e caminhos não Unicode exigem uso direto deTextEncoder/TextDecoderpara serem tratados corretamente. Pela minha experiência antiga, quando um servidor enviava texto em Shift_JIS e eu lia comresponse.text(), em tempo de execução só saía uma string vazia. Se você não estiver acostumado com problemas de codificação, pode facilmente perder dias depurando esse tipo de situação. Além disso, o exemplo em JS tem bugs e erros de sintaxe que não existem no código Rust (no loop, precisa defor-ofem vez defor-in). Também não dá para dizer que esse exemplo usa apenas “funções de primeira classe”; como em Rust, também é preciso entender iteradores, e ainda está usando CommonJS. Fora isso, ainda é preciso aprenderasync/await, Promises e top-level await, e o top-level await só recebeu suporte recentemente em alguns runtimes, incluindo Node. Ainda não é suportado por alguns engines JS, como o Hermes do React NativeEsse tipo de coisa é exatamente por que continuo usando Rust. O exemplo é só um caso, mas essas pequenas armadilhas e pegadinhas estão sempre espalhadas por outras linguagens. Individualmente talvez não aconteçam, mas ao longo do ciclo de vida inteiro de um programa elas se acumulam, e bugs estranhos continuam aparecendo de algum lugar, exigindo investigação constante. Em Rust isso não acontece. O sistema de tipos bloqueia antecipadamente uma quantidade absurda de casos. Na prática, depois que eu lanço um software com funcionalidades prontas em Rust, de vez em quando só adiciono recursos novos, e o esforço de corrigir bugs comuns quase desaparece. Claro, bugs lógicos podem existir em qualquer lugar, mas ele elimina na origem problemas vindos de incompatibilidades bobas de tipo/estrutura, então produtividade e manutenção acabam sendo uma experiência completamente diferente
Pessoalmente, sinto que não há tantos desenvolvedores em JS/TS que realmente entendam de verdade
thenable/Promise easync-await. Já vi coisas assim:Envolvem um wrapper em formato de callback dentro de uma Promise e depois usam isso de novo dentro de uma função
async. Sempre me dói por dentro. De fato vejo esse tipo de código em vários lugares. Quando você ainda leva em conta import de módulos,async import(), transpile, code splitting etc., fica realmente muito complexorustfmt, dorust-analyzer, correções de bugs norustce melhorias no relatório de erros do Cargo. Eu mesmo escrevo todos os dias scripts de reprodução de issues com cargo script-Zscript. Está em andamento desde 2023, e há issues abertas que parecem bem próximas de ficar prontas. Também vi no repositório do ZomboDB que o pipeline de build é tratado com Rust, embora eu não tenha entendido completamente o contexto todo. Quero mencionar que o frontmatter do cargo é extremamente útil para portabilidade de scripts. Basta compartilhar um único arquivo, e é possível baixar e usar dependências imediatamente, sem instalação ou inicialização extra, como acontece em Python ou Node.js#!/some/pathsimplesmente é executado pelo shell passando o arquivo inteiro para o comando especificado via stdinasynceconst. Então seria melhor dizer diretamente que “o Rust anterior à entrada deasynceconstera menor e mais limpo”, mas senti falta dessa explicação mais direta no textoCopy trait, reborrowing,deref coercion,into_iterautomático em loops, chamada automática dedropao final do escopo (isso poderia ser chamado manualmente ou então o compilador dar erro),:Sizedimplícito por padrão em trait bounds, elisão de lifetimes, ergonomia dematche várias outras automações/conveniências, seria possível ter um Rust verdadeiramente mais simples em sentido mecânico. Mas uma linguagem assim seria muito desconfortável para uso cotidiano. A ironia é que esses elementos foram pensados justamente para iniciantesasynceconst. Não fui mais direto porque tenho muitos amigos que trabalham nessas funcionalidades. O Matklad expressou isso muito bem no lobste.rs. O Rust de 2015 era mais completo e mais consistente, mas a visão do Rust não é coerência total, e sim tornar-se uma linguagem útil para a indústriaDerefé aplicado..into()e a traitFromtornam conversões de tipo discretas demais. Há muitas funções de “conveniência” desse tipo também na biblioteca padrão. No fim, o tipo do objeto fica ambíguo, e conectar chamadas de função às implementações se torna difícil (embora a IDE ajude um pouco)implicit return) esconde o fluxo do programa e convida a erros. Também não gosto muito do operador de interrogaçãoconstmostram que aprender isso em Rust pode inclusive poupar o trabalho posterior de desaprender maus hábitos absorvidos em linguagens mais tradicionaismem, então para entender bem a estrutura das interfaces, vale a pena começar por std::mem