Por que um desenvolvedor Rust precisaria de sua própria linguagem? Mergulhando no mundo das DSLs
Imagine que você está escrevendo um parser para um arquivo de configuração complexo, implementando lógica de jogo ou descrevendo regras de negócio. Toda vez você esbarra no mesmo problema: o código se torna volumoso e a legibilidade cai. E se você pudesse criar uma mini-linguagem, especialmente ajustada para sua tarefa? Isso é uma linguagem específica de domínio (DSL em Rust).
Rust é um ambiente ideal para criar DSLs. Seu sistema de macros, poderosa inferência de tipos e custo zero de abstrações permitem construir linguagens que não perdem em desempenho para código escrito à mão. Neste artigo, vamos explorar como transformar Rust em um construtor de suas próprias linguagens e mostrar por que isso pode ser útil para você.
O que é uma DSL e por que ela é necessária em Rust?
DSL (Domain-Specific Language) é uma linguagem criada para resolver problemas em uma área de domínio específica. Diferente de uma linguagem de programação de propósito geral, uma DSL sacrifica a universalidade em prol da expressividade em um nicho específico. No ecossistema Rust, existem duas abordagens principais:
- DSLs internas (embutidas) — construídas sobre a sintaxe do Rust usando macros e encadeamento de métodos. Exemplo: a biblioteca
clappara análise de argumentos de linha de comando. - DSLs externas — uma sintaxe separada, que é analisada em tempo de execução. Exemplo:
regexousqlx.
O desenvolvimento de DSLs em Rust é especialmente relevante para:
- Arquivos de configuração (YAML/TOML como DSLs externas)
- Motores de jogos (descrição do comportamento de NPCs)
- Frameworks de teste (sintaxe própria para cenários)
- Equilíbrio entre segurança e flexibilidade
Criando uma DSL interna em Rust: macros e encadeamentos
Vamos começar com algo simples — uma DSL interna para descrever requisições HTTP. Em vez de escrever:
let mut req = Request::new(Method::GET);req.set_url("https://api.example.com/data");req.add_header("Authorization", "Bearer token");req.add_header("Content-Type", "application/json");Queremos obter uma sintaxe concisa:
let req = request! { GET "https://api.example.com/data" headers { "Authorization": "Bearer token", "Content-Type": "application/json" }};Vamos implementar isso usando macros Rust — a ferramenta mais poderosa para criar DSLs internas:
macro_rules! request { ($method:ident $url:expr) => { Request::new(stringify!($method).parse().unwrap(), $url) }; ($method:ident $url:expr, headers { $($key:expr => $value:expr),* $(,)? }) => { { let mut req = Request::new(stringify!($method).parse().unwrap(), $url); $( req.add_header($key, $value); )* req } };}Essa abordagem é um exemplo clássico de linguagem específica de domínio em Rust: não criamos uma nova sintaxe, mas redefinimos a existente, tornando o código mais legível. Observe: a macro gera código Rust comum em tempo de compilação, portanto, erros de tipo são detectados antes da execução.
DSL interna avançada: padrão Builder
Macros não são a única maneira. Frequentemente, para criar DSLs em Rust, usa-se o padrão Builder com encadeamento de métodos. Vejamos um exemplo de DSL para consultas SQL:
let query = Query::select() .from("users") .where_clause("age > ?") .bind(18) .order_by("name") .build();A implementação de tal DSL é simples e elegante:
struct QueryBuilder { select: Vec<String>, from: String, condition: Option<String>, order: Option<String>,}\