Porque Rust é a linguagem que desenvolvedores backend estiveram esperando desde sempre
Eu não vou explicar o que é rust aqui, tem toneladas de conteúdo sobre isso já, então vamos direto ao ponto:
Macros
Vamos começar com a maior bruxaria do rust, os macros
Em linguagens como javascript, geralmente recorremos a programas externos que geram código, como o tsc (typescript), jsx (react), o antigo babel e etc... mas isso tem uma desvantagem, é preciso passar por um passo de pre processamento antes de executar o código.
No rust temos macros, podemos criar macros de maneira declarativa, que recebe alguns argumentos e faz um substituição simples parecido com macros em linguagens como C e C++, mas aqui entra a bruxaria, proc macros:
Proc macros são nada mais nada menos que funções que são executadas em tempo de compilação, o compilador compila essas funções e executa elas em tempo de compilação, e tudo que elas fazem é receber um punhado de tokens e cospe tokens, mas o fato de ser uma função, nos dá a possibilidade de interagir com o mundo externo, vamos olhar para um exemplo de biblioteca que faz uso desta funcionalidade.
SQLx, Cliente para banco de dados SQL
A biblioteca SQLx é um cliente para bancos de dados sql, como postgres, mysql, sqlite, entre outros.
A grande sacada do SQLx é que ele consegue se conectar no banco de dados em tempo de compilação e testar as queries no seu banco de dados local, vamos dar uma olhada nela:
let pool = PgPoolOptions::new().max_connections(5).connect("postgres://postgres:password@localhost/test").await?;
let user_id = "us_KnnOjOHsfy4454hLY3f22JutkuygK865uhy";
// Nenhum problema
let user = sqlx::query!("SELECT * from users WHERE id = $1", user_id)
.fetch_one(&pool)
.await
.expect("Usuário não foi encontrado ou houve algum problema enquanto a query executava");
Aqui conectamos num banco de dados postgres com uma tabela chamada users e fazemos uma query simples de select, se o banco de dados retornar exatamente 1 resultado, o sqlx vai armazenar dentro da variavel user, e a macro query!
inclusive gerou uma struct dentro do macro para tipar essa variavel e conseguimos ter auto complete sem precisar chamar ferramentas externas diretamente para gerar código 🎉, o valor retornado vai ter todas as colunas que o banco retornar nessa query, não é necessáriamente o schema da tabela, dependende do que pedir pro banco dentro da query e o sqlx te dá a liberdade de fazer o que você quiser.
Agora vamos ver o que acontece quando cometemos erros de sintaxe ou tipagem na query:
// Isso não compila:
let user = sqlx::query!("INSERT INTO users(year) VALUES ('Tiago Dinis') RETURNING *", user_id)
.fetch_one(&pool)
.await
.expect("Usuário não foi encontrado ou houve algum problema enquanto a query executava");
Dá um erro aqui porque estamos tentando colocar uma string numa coluna que espera ter um numero, o sqlx conectou no nosso banco de dados local e testou a query numa transaction, o banco retornou um erro porque o tipo da coluna year INT
não bate com o tipo do valor especificado para ser inserido ('Tiago Dinis'
), e o sqlx vai dar erro e parar a compilação.
Se precisar de usar as suas proprias structs, você pode criar uma struct normalmente sem nenhum derive, e usar a macro query_as!
inves de query!
e passar o nome da struct no primeiro parametro assim: sqlx::query_as!(User, "SELECT ...", params...)
Com isso, o sqlx consegue completamente substituir um ORM completamente, ORMs hoje em dia não sao usados pra conseguir trocar de banco facilmente, são usados para fazer a conversão das colunas em objetos da propria linguagem, e o sqlx consegue fazer isso muito bem.
Veja mais sobre o sqlx no repositorio no github: https://github.com/launchbadge/sqlx
Se ficou preocupado que isso possa aumentar a complexidade para compilar o projeto, o sqlx tem a feature offline
.
Você primeiro vai precisar instalar a CLI do sqlx com o comando cargo install sqlx-cli
, após isso, ao rodar o comando cargo sqlx prepare
, o sqlx vai gerar um arquivo chamado sqlx-data.json
, com as informações de todas as queries do projeto, e precisa ser rodado sempre que adiciona ou remove alguma query no projeto, na sua maquina não precisa rodar toda hora, so vai precisar quando for commitar, e lembresse de comitar esse arquivo no git, é extremamente importante!
O sqlx vai ler esse arquivo caso a variavel DATABASE_URL
não esteja definida ou SQLX_OFFLINE=1
, e assim ele permite que você compile o projeto mesmo sem ter um banco de dados rodando.
Tambem recomendo que se for utilizar esta funcionalidade, coloque o comando cargo sqlx prepare --check
na sua pipeline/CI para verificar se o arquivo sqlx-data.json
está atualizado.
Error Handling
Exceptions são a pior coisa inventada na história, ainda mais no javascript! já não é a primeira vez que tento debuggar um bloco de catch porque nao sei qual é o tipo de erro e só quero fazer uma determinada coisa so em um caso de erro especifico.
Erros não deveriam ser excecionais, deveriam ser esperados, é um efeito colateral de interajir com o mundo externo.
Isso é uma coisa que GoLang tambem acertou muito, retornar os erros como valores das funções, mas, isso não é suficiente.
O rust lida com os erros com o enum Result<T, E>
, onde T
é o valor retornado no caminho feliz, e o E
sendo o valor de Erro.
Enums no rust são um pouco diferentes dos enums de outras linguagens como C, C++ ou TypeScript, vamos ver como o enum Result
é definido na std library:
enum Result<T, E> {
Ok(T),
Err(E)
}
Você: Ue? mas o que é esse (T) e (E)?
Pois é meu amigo, as variantes dos enums do rust podem armazenar valores dentro, isso mesmo! você não precisa fazer uma struct com um tipo e depois todas as fields, em que algumas so sao válidas em um certo estado!
Se recebermos um valor do tipo Result
, não sabemos se é Ok(T)
ou Err(E)
, e o rust vai nos obrigar a verificar qual dos dois nós recebemos, e como verificamos isso? E é ai que vem o próximo ponto deste post:
Modelando informações sem OOP, Pattern matching, e fazendo com que estados invalidos sejam inrepresentaveis.
Vou colocar um exemplo do Tris do canal No Boilerplate, que inclusive recomendo assistir se quiser saber ainda mais sobre o que estou falando aqui nesse post.
O Tris, enquanto olhava um manual de um jogo do mario, encontrou uma maquina de estados assim:
Podemos modelar os estados deste sistema com um enum:
#[derive(Debug, PartialEq)]
enum State {
Mario,
SuperMario,
CapeMario,
FireMario
}
Agora vamos modelar os powerups que o mário pode coletar e que faz o mario transitar entre os estados:
#[derive(Debug, PartialEq)]
enum PowerUp {
Feather,
Flower,
Mushroom
}
E agora podemos tambem criar a nossa struct para o jogador, com o estado que modelamos antes:
#[derive(Debug)]
struct Player {
state: State,
lives: u16,
}
Agora entra a mágica, vamos criar uma função que é chamada quando o mario coleta um power up, e faz o mario mudar de estado:
impl Player {
pub fn new() -> Self {
Self { state: State::Mario }
}
pub fn collect(&mut self, collected_power: PowerUp) {
match (&self.state, collected_power) {
(Mario, Mushroom) => self.state = SuperMario,
(_, Flower) => self.state = FireMario,
(_, Feather) => self.state = CapeMario,
(_, Mushroom) => self.lives += 1
}
}
}
Aqui utilizamos o match
, que é similar a um switch
, mas é muito mais poderoso, ele pode desconstruir objetos e verificar valores como nenhuma outra linguagem faz.
No match criamos uma tupla com uma referencia para o estado no primeiro elemento, e o power up no segundo elemento, e nós verificamos dentro do match cada possibilidade, e o compilador vai obrigar o match a ser exaustivo, ou seja, ele precisa lidar com todas as possibilidades, e é assim que nunca mais temos edge-cases.
Velocidade
Isso não é nenhuma surpresa para ninguem, mas o fato que conseguimos ter todas essas funcionalidades e ainda ter uma performance muito proxima de C, é simplesmente incrivel, é pena as pessoas so não terem visto todo esse poder ainda.
Ferramentas
Se você quiser usar rust para o seu próximo projeto, você tem tudo que precisa para começar, o ceu é o limite, todas as ferramentas para construir aplicações, APIs, CLIs, não está no futuro, elas estão agora presentes neste exato momento.
Frameworks web: actix
, axum
Client de SQL: sqlx
ORMs: sea-orm
, diesel
Frontend (webassembly): Dioxus, Yew, Leptos
Pagamentos com stripe: async-stripe
E muito mais!
Conclusão
E com isto termino este post gigantesco, onde mostro que rust está mais que pronto para ser usado nos seus proximos projetos e que vai ajudar a ter menos bugs nas suas aplicações.
Tem alguma coisa para dizer? Quer usar rust no seus proximos projetos? Ou você nem sequer é programador e não tem a minima ideia do que ta fazendo? Não deixe de deixar um comentário aqui!