Pourquoi un développeur Rust aurait-il besoin de son propre langage ? Plongée dans l'univers des DSL
Imaginez que vous écrivez un parseur de fichier de configuration complexe, que vous implémentez une logique de jeu ou que vous décrivez des règles métier. À chaque fois, vous vous heurtez au même problème : le code devient lourd et la lisibilité diminue. Et si vous pouviez créer un mini-langage, spécialement conçu pour votre tâche ? C'est ce qu'on appelle un langage dédié (DSL en Rust).
Rust est un environnement idéal pour créer des DSL. Son système de macros, sa puissante inférence de types et ses abstractions à coût nul permettent de construire des langages qui ne sont pas moins performants que du code écrit à la main. Dans cet article, nous verrons comment transformer Rust en un constructeur de vos propres langages, et pourquoi cela peut vous être utile.
Qu'est-ce qu'un DSL et pourquoi en a-t-on besoin en Rust ?
Un DSL (Domain-Specific Language) est un langage créé pour résoudre des problèmes dans un domaine spécifique. Contrairement à un langage de programmation généraliste, un DSL sacrifie l'universalité au profit de l'expressivité dans un créneau étroit. Dans l'écosystème Rust, il existe deux approches principales :
- DSL internes (embarqués) — construits au-dessus de la syntaxe de Rust à l'aide de macros et de chaînes de méthodes. Exemple : la bibliothèque
clappour l'analyse des arguments de ligne de commande. - DSL externes — une syntaxe distincte, analysée à l'exécution. Exemple :
regexousqlx.
Le développement de DSL en Rust est particulièrement pertinent pour :
- Les fichiers de configuration (YAML/TOML en tant que DSL externes)
- Les moteurs de jeu (description du comportement des PNJ)
- Les frameworks de test (syntaxe personnalisée pour les scénarios)
- L'équilibre entre sécurité et flexibilité
Créer un DSL interne en Rust : macros et chaînes de méthodes
Commençons par quelque chose de simple : un DSL interne pour décrire des requêtes HTTP. Au lieu d'écrire :
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");Nous voulons obtenir une syntaxe concise :
let req = request! { GET "https://api.example.com/data" headers { "Authorization": "Bearer token", "Content-Type": "application/json" }};Implémentons cela avec les macros Rust — l'outil le plus puissant pour créer des DSL internes :
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 } };}Cette approche est un exemple classique de langage dédié en Rust : nous ne créons pas une nouvelle syntaxe, mais nous redéfinissons la syntaxe existante pour rendre le code plus lisible. Notez que la macro génère du code Rust standard à la compilation, donc les erreurs de type sont détectées avant l'exécution.
DSL interne avancé : le pattern Builder
Les macros ne sont pas le seul moyen. Pour créer un DSL en Rust, on utilise souvent le pattern Builder avec des chaînes de méthodes. Prenons l'exemple d'un DSL pour les requêtes SQL :
let query = Query::select() .from("users") .where_clause("age > ?") .bind(18) .order_by("name") .build();L'implémentation d'un tel DSL est simple et élégante :
struct QueryBuilder { select: Vec<String>, from: String, condition: Option<String>, order: Option<String>,}