Executando verificação de segurança...
18

Metaprogramação e minha primeira experiência com Rust.

Você já se perguntou por que Rust é uma linguagem tão bem falada? Não apenas por sua performance, mas sempre está como "a linguagem mais amada" em pesquisas como as do Stack Overflow.

No início do ano fui convidado por um amigo a ajudá-lo na construção de alguns microsserviços, como estava querendo estudar uma nova linguagem, decidi arriscar e experimentar criar o meu em Rust. Foi meu primeiro contato com a linguagem, mas, já me trouxe ótimas impressões. Quero compartilhar essa experiência aqui.

Era um serviço muito simples: acessar um servidor FTP, processar arquivos de textos de notas fiscais com layout PROCEDA, mapear isso para um json e enviar para uma fila (RabbitMQ) onde outros serviços agiriam conforme.

Não havia regras de negócio muito complicadas, e parecia um ótimo caso para explorar bastante da nova linguagem.

Em alguns dias já deu pra entender o motivo da linguagem ser tão bem falada.

Todo mundo já está de saco cheio de saber da performance e segurança do Rust, minha abordagem aqui será outra: o "prazer do desenvolvedor" que a linguagem oferece.

Apesar de estranha no início, Rust é uma linguagem pouquissimo verbosa. Ela é realmente poderosa para realizar grandes coisas com poucas linhas de código. Tem uma semântica e uma sintaxe simples. Essa simplicidade, em certo ponto, me lembrou um pouco de Go. Poucos e simples arquivos de configuração, programação funcional, e catapimba. Rust é simples, mas está longe de ser fácil (abordaremos isso depois).

O que me encheu mesmo os olhos foi a liberdade que o Rust proporciona ao programador através de sua metaprogramação com macros e traits.

Sempre fui fã de criar ferramentas, e por isso sempre gostei de metaprogramação. Para quem não sabe, metaprogramação é basicamente programar ferramentas que ajudam a escrever programas. Como um bom Javeiro, sempre foi algo que me encantou: pensar em como ferramentas como o Spring traz abstrações que ajudam nossa vida, gerando código em tempo de compilação, por exemplo.

O Rust tem muito disso. E é incrível.

Existem os "macros" que são simplesmente snippets que geram códigos para você. Os macros são uma mão na roda para você pular boilerplates e focar no que realmente importa. Isso simplifica ainda mais.

No meu caso, eu criei um macro (vou mandar o repositório da biblioteca mais tarde), que me ajudava a transformar os arquivos de texto em "structs" (objetos no rust).

Basicamente, cada linha era um objeto. E cada propriedade do objeto estavam posicionados entre um range predefinido de caracteres.

Por exemplo, digamos que a linha 10 é para o objeto "pessoa".

  • O nome da pessoa estaria dentro dos primeiros vinte caracteres dessa linha.
  • O e-mail dela, estaria do caracter 21 até o 50 dessa linha.
  • O documento dela estaria do caracter 51 até o 70 dessa mesma linha.
  • O endereço dela estaria dos caracteres 71 até o 150 dessa linha.

Por ai vai.

Eu poderia escrever tudo isso na mão, com código puro, sim. Mas considerando o número de dados que uma nota fiscal possui, daria um trabalhão, certo?

Por sorte, eu sou um programador, e crio programas para evitar trabalho braçal repetitido... e... como minhas ferramentas são programas, posso fazer isso com o meu próprio trabalho! Incrível, não é?

A forma de solucionar isso com o Rust foi simples. Eu criei um macro para gerar código para mim a partir de um "decorator". Já explicarei o que é um decorator.

Vamos exemplificar melhor com código, o código abaixo é uma struct no Rust, uma "classe":

struct Pessoa {
    pub nome: String,
    pub email: String,
    pub documento: String,
    pub endereco: String,
}

É fácil de entender, é um objeto do tipo pessoa, que tem nome, email, documento e endereço que são strings.

Decorators são anotações que colocamos no nosso código e é a grande ferramenta da metaprogramação. Em algumas linguagens são chamados de Annotations. Lembra? No java ou no typescript usamos isso com @, como o @Entity, @Controller, no C# usamos entre [ e ].

No Rust é um pouco parecido com o C# (que é uma linguagem que também me fascina muito no quesito de liberdade que dá ao programador).

Eu criei um decorator que é o seguinte: #[from_line(X..Y)], X é o primeiro caracter que vai pegar da linha, e o Y é o último (na verdade, o Y é excluido, então vai pegar do X até o caracter antes do Y).

Então, no final, bastava eu apenas marcar minha struct assim:

#[derive(FromLine)]
struct Pessoa {
    #[from_line(0..20)]
    pub nome: String,
    #[from_line(20..49)]
    pub email: String,
    #[from_Line(49..69)]
    pub documento: String,
    #[from_line(69..149)]
    pub endereco: String,
}

E com isso, o Rust me gera uma função Pessoa::from_line(line), que eu posso passar a linha ali dentro e ele já me retorna a struct formada.

Só que existe uma outra questão, algumas Structs tinham outros tipos de dados que eu gostaria de preservar: inteiros, decimais, booleanos. Alguns dados eram opcionais, outros não.

Então, para fazer isso, eu implementei uma trait. Trait para Rust é como se fosse uma interface. Só que existe algo muito incrível que eu não vi em nenhuma outra linguagem:

Traits podem ser implementadas mesmo após a declaração de uma "classe" ou "objeto".

Ou seja, você pode criar uma trait e implementar para tipos nativos do Rust. Você cria um método e implementa ao tipo "inteiro" do Rust, e esse "inteiro" agora vai ter um novo método que você criou. Isso para mim abriu muitas possibilidades, porque eu poderia não só pegar tipos da biblioteca nativa do Rust, mas também de outras bibliotecas, e implementar minha trait neles.

E assim, eles se tornaram adaptados ao meu macro.

Eu não posso compartilhar o código original, mas fiz uma biblioteca para exemplificar como seria isso, no repositório abaixo:

https://github.com/gmessiasc/from_line/tree/main

Bom, mas nem tudo é flores. Rust é realmente difícil, e me deixou triste em alguns momentos: você precisa a todo momento saber o que está fazendo, o que o seu codigo vai fazer a nível de memória, é difícil compilar porque qualquer coisinha errada você toma bronca do Borrow Checker, e parece que não tem nenhuma inteligência artificial inteligente o suficiente para te ajudar nessa linguagem (o copilot e o bing.ai se sairam melhores, mas foram raras as vezes que eles me salvaram). Passei por estresses e perrengues devido minha falta de experiência, às vezes para fazer coisas simples.

Mas sinceramente? Os pontos positivos para mim foram muito mais impactantes, e me encantei com a linguagem. A visão que eu tenho é que com a experiência, sabendo exatamente o que vocÊ está fazendo, você consegue ser tão produtivo quanto uma linguagem mais "fácil" como python, javascript ou java, e, produzir aplicações performáticas e realmente potentes.

Carregando publicação patrocinada...
5

Rust é uma linguagem maravilhosa, estou no meu 6º ou 7º projeto com ela, 4 deles em produção, 1 lib publicada (aqui) e um jogo escrito usando a lib bevy. De fato rust é uma linguagem que consegue trazer muita produtividade quando se entende os conceitos da linguagem, mas o meu ponto preferido da linguagem é o fato de não ter null/undefined e se vc programa em rust pode pensar que o "None" seria equivalente e pode até ser, mas a forma com que a linguagem trata o "None" é bem diferente e vou fazer uma analogia aqui para tentar explicar.

Suponha que fizemos um SELECT WHERE num banco de dados usando uma linguagem qualquer, como costume salvamos esse resultado numa variável e agora essa variável se tornou o que eu carinhosamente apelidei de Variável de Shrodinger, pois ela pode ter um resultado ou pode ter um null/undefined/None, até que seja feito algo como um "if" para verificar o conteúdo, essa variável é um mistério. Numa linguagem como JS/TS (minha linguagem de trabalho hoje) cada if é similar a abrir a caixa e olhar o que tem dentro, talvez tenha um objeto com os valores da consulta, talvez tenha um null, se você precisa fazer várias verificações desse retorno, é como passar essa caixa pessoa por pessoa e cada 1 olha o conteúdo e passa para outra. Em rust essa verificação é diferente, rust não deixa vc abrir a caixa olhar e fechar, ele abre a caixa em cima de você e você que lide com esse resultado, ele não te devolve a caixa, isso obriga uma tratativa imediata desse resultado, a Variável de Shrodinger deixa de existir e você precisa criar uma nova indicando explicitamente que é uma variável com conteúdo.

TypeScript:

const result = await postgres.findByEmail("[email protected]"); // e se rolar um throw Error aqui?
if (result === null) {
    return "email não encontrado";
}
return anyFunction(result); // será que essa função vai receber undefined algum dia?

Rust:

let result = postgres.findByEmail("[email protected]").await;
match result {
    Ok(value) => anyFunction(value),
    Err(error) => format!("falha ao buscar usuário: {}", error)
}

Claro que é possível ter o mesmo resultado em qualquer linguagem, só que linguagens como o JS te permitem ser negligente (e muitos devs são), no exemplo anterior eu deveria usar um try/catch em volta do código mas o JS não se importa com isso...

TypeScript:

try {
    const result = await postgres.findByEmail("[email protected]");
    if (result === null) {
        return "email não encontrado";
    }
    return anyFunction(result);
} catch(error) {
    return `falha ao buscar usuário: ${error.message}`
}

Rust não te deixa ser negligente tão fácilmente, vc precisa forçar a barra para fazer código ruim e mesmo que decida ir por esse caminho, quando ler o código você vai se sentir incomodado. Outro ponto é que Rust não é uma linguagem feita para entrar em estado de erro, se você programa em rust e faz o seu código com foco em executar seu binário e nunca ver ele entrar em pânico (expressão da linguagem) saiba que você entendeu e está no caminho certo.

Ah, um ultimo detalhe, realmente o cenário de rust aqui no Br está muito fraco, por conta disso estou começando um canal no YT e os primeiros vídeos já serão uma biblioteca em rust que será publicada no crates.io, vou tentar mostrar tudo, testes automatizados, documentação, processo do crates.io, baixar essa lib para usar em outros projetos, etc... e é isso, obrigado por ter lido até aqui.

2

Isso pelo que vejo ele ta encapsulando o retorno como Error Monad, isso vem de linguagens funcionais como Haskell. O Rust herda conceitos de Result, Maybe.. etc isso junto com as garantias que ele fornece tornam a linguagem poderosa.

2

Na verdade o borrow checker te salva de coisas que tem em linguagens como javascript que ninguem de conta como data races por exemplo, compartilhar referencias mutaveis é comportamente indefinido e ponto final. você deve sempre travar pra apenas ter 1 referencia mutavel por vez em qualquer linguagem e o borrow checker te salva disso.

E tambem tem que entender que não tem um runtime tomando conta de tudo, então você tá mexendo no nivel de um C++ da vida, por isso que existe várias maneiras de gerenciar memoria e vários "ponteiros inteligentes", que inclusive até existe similares no c++, so que não tem as verificações de segurança do rust em tempo de compilação.

1

Com certeza! Talvez eu devesse editar essa parte para ficar mais claro.

Quando eu digo sobre a dificuldade, não é um demérito ou crítica - até porque faz todo o sentido diante da abordagem da linguagem, e é algo ótimo para preservar a segurança e até mesmo ensinar o programador (as mensagens de erro do compilador são bem intuitivas). O que eu tava querendo passar era sobre a experiência, o que senti enquanto codava, e querendo ou não é frustrante "tomar bronca" do borrow checker (como eu falei). Não por demérito da linguagem, mas porque eu não tinha conhecimento ainda para entender. Perder tempo tentando entender esse fluxo para fazer coisas simples é algo que frustra.

Então é normal entender porque pessoas com menos tolerância a frustração acabam desistindo de aprender. No meu caso, foi desafiador e é algo que eu gosto, cativou mais, como se fosse um jogo.

Mas sim, o borrow checker é incrível porque te força a fazer a coisa correta. É o tipo de "mãe" rigorosa que cuida do filho e educa ele para ser o mais certinho possível, sem mimá-lo. Isso não é algo ruim, mas as vezes pode ser frustrante ou difícil para o filho. Só que se ele passar por essa etapa, com certeza será uma pessoa melhor. No caso do rust, um programador melhor.

1
1

Na documentação oficial da linguagem tem este link, com traduções em vários idiomas. Em português, é esta aqui.

Não vi como está a qualidade da tradução, então vá por conta e risco. Eu pessoalmente prefiro ler este tipo de conteúdo em inglês, porque a maioria do material oficial é sempre escrito originalmente neste idioma, mas entendo que muitos preferem versões traduzidas por diversos motivos.

2
1

Opa meu mano, eu acho que realmente falta conteudo em português, a maioria dos videos e conteudos que me ajudaram foram em inglês.

Mas, tem esses dois caras brasileiros que ajudaram muito:

https://www.youtube.com/live/aXQenZGvLrQ?si=gn-xdMD5roO-qVqe

Navarro que ficou entre os melhores da Rinha de Backend, gravou esse video como que ele fez a api dele. A didática dele é muito boa.

Mas caso queira começar bem do basico a partir da parte mais teorica tem o CodeShow:

https://youtube.com/playlist?list=PLjSf4DcGBdiGCNOrCoFgtj0KrUq1MRUME&si=1cIrjoxiJxqA9urg

De resto, recomendo muito você olhar documentações oficiais, pesquisar bastante na internet alguns exemplos e repositorios do github.

1

Não é por nada mas acho que era boa ideia aprender inglês, vai desbloquear pelo menos 80% da internet. Eu aprendi ingles na raça, pela minha sobrevivencia, e a algum tempo (1 ano) comecei até a aprender japonês(実は日本語が大好きです!)e eu tenho só 16 anos ainda.
Não tem nenhum motivo pra não aprender,você tem o google, youtube, a informação vc ja tem, é só pescar.

1

Gosto bastante de Rust também, mas confesso que a sintaxe dele me irrita e a forma de criar macros irrita ainda mais, digo isso porque já trabalhei com diversas linguagens e já vi diversos conceitos em Python, Ruby, Elixir, JavaScript, C#..

Acho que algumas coisas poderiam ser "simplificadas" na sintaxe entende? Você pode pegar o Elixir por exemplo, a sintaxe dele é extremamente clara, você também pode criar macros lá, mas a diferença é que você entende seus macros e os macros dos outros..

Se você abrir um repo você entende claramente o que aquilo está fazendo e no Rust o cerébro trava, a sintaxe não é convencional, as macros sao esquisitas.. haha

Mas eu adoro a linguagem e continuo criando coisas com ela, minha opinião é mais em relação a sintaxe da linguagem.