games / lua
Concevoir un DSL en Lua
Explication
Ce que vous allez apprendre
- Comprendre ce qu'est un DSL (Domain Specific Language) et pourquoi Lua s'y prête bien
- Utiliser le sucre syntaxique
f{...}pour écrire une configuration déclarative - Construire un DSL "builder" fluide en faisant retourner
selfpar chaque méthode - Écrire un mini-DSL de validation de données façon schéma
- Toujours prévoir un moyen d'inspecter ce qu'un DSL a réellement construit
Dans quel contexte ?
Un studio veut que ses game designers, qui ne sont pas tous programmeurs Lua expérimentés, puissent définir de nouvelles routes de commandes ou des règles de configuration serveur sans écrire du code Lua impératif classique. Un DSL bien conçu transforme une déclaration presque lisible en langage naturel en une vraie structure Lua exploitable par le moteur, réduisant la barrière d'entrée pour ces contributeurs non spécialistes.
| Technique Lua exploitée | Ce qu'elle permet dans un DSL |
|---|---|
f{...} (sans parenthèses) | Syntaxe de configuration déclarative |
return self dans une méthode | Chaînage fluide façon "builder" |
Métatables (__index, __call) | Comportement personnalisé transparent |
Astuce
Toujours écrire une fonction de "dump" qui affiche la structure réelle produite par ton DSL. Sans elle, déboguer un DSL expressif revient à deviner ce qu'il a construit, ce qui devient vite intenable dès que la configuration grandit.
Qu'est-ce qu'un DSL, et pourquoi Lua s'y prête particulièrement bien
Un DSL (Domain Specific Language, "langage dédié à un domaine") est un mini-langage conçu pour exprimer un besoin précis de façon lisible, plutôt que d'utiliser la syntaxe générale complète du langage hôte — par exemple, décrire une configuration de serveur ou un ensemble de routes HTTP d'une manière qui ressemble presque à un format déclaratif dédié. Lua se prête naturellement à cet exercice grâce à deux caractéristiques syntaxiques particulières, déjà vues séparément dans ce cours mais combinées ici pour un usage précis.
Le sucre syntaxique f{...} : la base de tout DSL déclaratif
Lua permet d'appeler une fonction avec une seule table en argument sans écrire les parenthèses (Config{...} équivaut à Config({...})). Ce détail syntaxique, presque anodin en apparence, est en réalité l'ingrédient central qui permet d'écrire des configurations qui ressemblent à un format de données déclaratif plutôt qu'à du code impératif classique.
Les métatables au service d'un style "fluide"
En faisant retourner self par chaque méthode d'un objet (un pattern déjà vu dans la leçon sur la programmation orientée objet), on obtient un DSL "builder" où les appels s'enchaînent naturellement (router:get(...):post(...)), lisible presque comme une description de haut niveau du comportement souhaité, plutôt que comme une suite d'instructions séparées.
Le piège à éviter : un DSL trop opaque
Plus un DSL est expressif et "magique", plus il devient difficile de comprendre exactement ce qu'il produit réellement en cas de problème. La bonne pratique consiste à toujours prévoir un moyen d'inspecter ce que le DSL a construit (une fonction de "dump" qui affiche la structure réelle obtenue) — sans cela, déboguer un DSL revient à deviner, ce qui devient vite insoutenable dès que la configuration grandit.
Le lien avec la leçon suivante
Un DSL de validation (comme le schéma de données présenté ici) centralise des règles qui, sans lui, se disperseraient dans le code appelant — une préoccupation de fiabilité qui rejoint directement le sujet des tests unitaires abordé ensuite : un comportement centralisé et prévisible est bien plus facile à tester qu'une logique éparpillée.
Commandes & code
Concevoir un DSL en Lua
Lua se prête naturellement à l'écriture de DSL (Domain Specific Language) grâce à sa syntaxe de table permissive et ses métatables.
-- Syntaxe Lua exploitable pour un DSL : appel de fonction avec une seule table = pas besoin de parenthèses
-- f{...} est équivalent à f({...})
local function Config(t) return t end
local serverConfig = Config {
name = "Technologik RP",
maxPlayers = 64,
difficulty = "hard",
}
-- Résultat : une vraie table Lua, lisible comme un fichier de config déclaratif, sans parser custom-- DSL de définition de routes HTTP façon "builder", en s'appuyant sur les métatables pour le chaînage
local Router = {}
Router.__index = Router
function Router.new()
return setmetatable({ routes = {} }, Router)
end
function Router:get(path, handler)
table.insert(self.routes, { method = "GET", path = path, handler = handler })
return self -- retourne self : permet le chaînage fluide façon DSL
end
function Router:post(path, handler)
table.insert(self.routes, { method = "POST", path = path, handler = handler })
return self
end
-- Usage : lecture quasi déclarative, comme un mini-langage de routage
local router = Router.new()
:get("/players", function() return "liste des joueurs" end)
:post("/players", function() return "création joueur" end)
:get("/players/:id", function(id) return "détail joueur " .. id end)
for _, route in ipairs(router.routes) do
print(route.method, route.path)
end-- DSL déclaratif avancé avec validation intégrée : schéma de données façon "mini-JSON Schema"
local function Field(type_, opts)
opts = opts or {}
return { type = type_, required = opts.required or false, default = opts.default }
end
local PlayerSchema = {
name = Field("string", { required = true }),
level = Field("number", { default = 1 }),
vip = Field("boolean", { default = false }),
}
local function validate(schema, data)
local result = {}
for fieldName, def in pairs(schema) do
local value = data[fieldName]
if value == nil then
if def.required then
error(string.format("Champ requis manquant: '%s'", fieldName))
end
value = def.default
elseif type(value) ~= def.type then
error(string.format("Type invalide pour '%s': attendu %s, reçu %s", fieldName, def.type, type(value)))
end
result[fieldName] = value
end
return result
end
local player = validate(PlayerSchema, { name = "Alice", level = 42 })
print(player.name, player.level, player.vip) -- Alice 42 false (valeur par défaut appliquée)-- Piège niveau expert : un DSL trop "magique" complique le débogage. Toujours prévoir un mode de dump/inspection.
local function dumpDSL(t, indent)
indent = indent or ""
for k, v in pairs(t) do
if type(v) == "table" then
print(indent .. tostring(k) .. ":")
dumpDSL(v, indent .. " ")
else
print(indent .. tostring(k) .. " = " .. tostring(v))
end
end
end
dumpDSL(serverConfig) -- permet de vérifier ce que le DSL a réellement produit, sans devinerRésumé
- La syntaxe
f{...}(appel avec une seule table) est la base de la plupart des DSL déclaratifs en Lua. - Les métatables permettent de construire des DSL fluides façon "builder" via le chaînage de méthodes (
return self). - Un DSL de validation (type schema) centralise les règles métier au lieu de les disperser dans le code appelant.
- Un DSL doit toujours rester inspectable (fonction de dump) pour éviter de déboguer à l'aveugle.
Exercices pratiques
Mission : le routeur DSL qui perd une route en silence
Objectif : Diagnostiquer un bug de chaînage dans un DSL de type builder et renforcer sa capacité d'inspection pour les game designers non-développeurs.
Contexte
Le studio a mis en place un DSL de routage façon builder pour ses game designers, mais un designer signale que sa route "POST /players" a disparu :
local Router = {}
Router.__index = Router
function Router.new()
return setmetatable({ routes = {} }, Router)
end
function Router:get(path, handler)
table.insert(self.routes, { method = "GET", path = path, handler = handler })
-- oubli : pas de "return self" ici
end
function Router:post(path, handler)
table.insert(self.routes, { method = "POST", path = path, handler = handler })
return self
end
local router = Router.new()
:get("/players", function() end)
:post("/players", function() end) -- erreur: attempt to call a nil value (method 'post')Le designer ne comprend pas pourquoi enchaîner :post() juste après :get() fait planter le script.