Polar Code 🎭

Command Palette

Search for a command to run...

30
Pièce N°30

CHAPITRE 29 – INTERPRETER

Le code morse tapé sur le vieux transmetteur : « ••• •−−− ••• » (SOS). Un opérateur expérimenté l’entendait et comprenait immédiatement : détresse. Le système devait interpréter ce langage limité, cette grammaire de points et de traits. Pas un langage de programmation complet, juste un micro-langage pour exprimer des commandes ou des requêtes. C’était le domaine de l’Interpreter.

Langages simples

Le pattern Interpreter permet d’évaluer des phrases dans un langage donné, en représentant la grammaire de ce langage comme une hiérarchie d’objets. Il est utile pour des langages simples ayant une grammaire définie : expressions mathématiques, requêtes de filtrage, langages de configuration, formules logiques, protocoles de communication simples.

Ce n’est pas pour construire un compilateur ou un interpréteur de langage général (comme Python ou JavaScript). Pour cela, on utilise des outils plus puissants (parser generators, ANTLR). L’Interpreter est pour les DSL (Domain Specific Languages) légers.

AST (Abstract Syntax Tree)

Le cœur du pattern est la construction d’un Arbre de Syntaxe Abstraite (AST). Chaque règle de la grammaire est représentée par une classe. Un noeud de l’arbre est une expression qui peut s’évaluer.

Exemple : une grammaire pour des expressions booléennes simples avec AND, OR, NOT, et des variables.

// --- LA GRAMMAIRE (AST) ---
// Expression : l'interface de base pour tous les noeuds de l'arbre.
interface Expression {
    interpret(context: Context): boolean;
}

// Expressions terminales (feuilles)
class Variable implements Expression {
    constructor(private name: string) {}
    interpret(context: Context): boolean {
        return context.get(this.name);
    }
}

class Constante implements Expression {
    constructor(private value: boolean) {}
    interpret(context: Context): boolean {
        return this.value;
    }
}

// Expressions non terminales (noeuds)
class Et implements Expression {
    constructor(private left: Expression, private right: Expression) {}
    interpret(context: Context): boolean {
        return this.left.interpret(context) && this.right.interpret(context);
    }
}

class Ou implements Expression {
    constructor(private left: Expression, private right: Expression) {}
    interpret(context: Context): boolean {
        return this.left.interpret(context) || this.right.interpret(context);
    }
}

class Non implements Expression {
    constructor(private expr: Expression) {}
    interpret(context: Context): boolean {
        return !this.expr.interpret(context);
    }
}

// Contexte : fournit les valeurs des variables
class Context {
    private variables: Map<string, boolean> = new Map();
    set(name: string, value: boolean): void {
        this.variables.set(name, value);
    }
    get(name: string): boolean {
        if (!this.variables.has(name)) {
            throw new Error(`Variable inconnue: ${name}`);
        }
        return this.variables.get(name)!;
    }
}

// --- PARSER (simplifié à mort) ---
// Dans la vraie vie, on utilise un vrai parser (recursive descent, etc.).
// Ici, on simule la construction de l'AST à la main pour l'exemple.
class ParserSimple {
    // Parse une expression comme "(alarmeActive AND porteOuverte) OR NOT systemeArme"
    // C'est TRÈS simplifié et ne gère pas la vraie syntaxe.
    static parse(tokens: string[], context: Context): Expression {
        // Algorithme naïf qui suppose une forme spécifique.
        // Un vrai parser est bien plus complexe.
        let i = 0;
        const parseExpression = (): Expression => {
            const token = tokens[i++];
            if (token === '(') {
                const left = parseExpression();
                const op = tokens[i++];
                const right = parseExpression();
                i++; // consommer ')'
                if (op === 'AND') return new Et(left, right);
                if (op === 'OR') return new Ou(left, right);
                throw new Error(`Opérateur inattendu: ${op}`);
            }
            if (token === 'NOT') {
                return new Non(parseExpression());
            }
            // C'est une variable ou une constante
            if (token === 'true') return new Constante(true);
            if (token === 'false') return new Constante(false);
            // Sinon, c'est une variable
            return new Variable(token);
        };
        return parseExpression();
    }
}

// --- SCÉNARIO : SYSTÈME D'ALARME INTELLIGENT ---
console.log("=== INTERPRÉTEUR D'EXPRESSIONS BOOLÉENNES (Interpreter Pattern) ===\n");

// Définition du contexte (état du système)
const contexteAlarme = new Context();
contexteAlarme.set('alarmeActive', true);
contexteAlarme.set('porteOuverte', false);
contexteAlarme.set('fenetreOuverte', false);
contexteAlarme.set('systemeArme', true);
contexteAlarme.set('mouvement', true);

console.log("État du système:");
contexteAlarme.set('alarmeActive', true);
contexteAlarme.set('porteOuverte', false);
contexteAlarme.set('fenetreOuverte', true);
contexteAlarme.set('systemeArme', true);
contexteAlarme.set('mouvement', false);

// Construction MANUELLE de l'AST (en vrai, on le ferait via un parser).
// Expression: (porteOuverte OR fenetreOuverte) AND alarmeActive AND systemeArme
const ast1 = new Et(
    new Ou(new Variable('porteOuverte'), new Variable('fenetreOuverte')),
    new Et(new Variable('alarmeActive'), new Variable('systemeArme'))
);

console.log("\n--- Règle 1: Alarme déclenchée si une entrée est ouverte ET alarme active ET système armé ---");
console.log(`AST: (porteOuverte OR fenetreOuverte) AND alarmeActive AND systemeArme`);
const resultat1 = ast1.interpret(contexteAlarme);
console.log(`Résultat: ${resultat1} → Alarme ${resultat1 ? '🚨 DÉCLENCHÉE' : 'silencieuse'}`);

// Autre expression: mouvement AND NOT systemeArme
const ast2 = new Et(
    new Variable('mouvement'),
    new Non(new Variable('systemeArme'))
);

console.log("\n--- Règle 2: Alerte si mouvement détecté ET système désarmé ---");
console.log(`AST: mouvement AND NOT systemeArme`);
contexteAlarme.set('systemeArme', false); // On désarme le système
const resultat2 = ast2.interpret(contexteAlarme);
console.log(`Résultat: ${resultat2} → Alerte ${resultat2 ? '⚠️ ACTIVÉE' : 'inactive'}`);

// --- Démonstration avec un "parser" simplifié ---
console.log("\n=== Utilisation avec un parser (très simplifié) ===");
// On simule des tokens pour l'expression: (alarmeActive AND porteOuverte) OR NOT systemeArme
const tokens = ['(', 'alarmeActive', 'AND', 'porteOuverte', ')', 'OR', 'NOT', 'systemeArme'];
try {
    const astParsed = ParserSimple.parse(tokens, contexteAlarme);
    console.log(`Expression parsée depuis tokens: ${tokens.join(' ')}`);
    const resultatParsed = astParsed.interpret(contexteAlarme);
    console.log(`Résultat: ${resultatParsed}`);
} catch (e) {
    console.error(`Erreur de parsing: ${e}`);
}

Cas très spécifiques

L’Interpreter est un pattern de niche. Vous saurez quand vous en aurez besoin :

  • Vous avez un langage simple mais structuré à interpréter.
  • La grammaire est fixe et limitée.
  • La performance n’est pas critique (l’interprétation est plus lente que l’exécution native).
  • Vous voulez facilement étendre la grammaire (ajouter un nouvel opérateur = ajouter une nouvelle classe).

Exemples réels :

  • Expressions de filtre dans une UI (age > 30 AND department = "Sales").
  • Calculatrice avec opérateurs et parenthèses.
  • Langages de script pour des jeux (commandes de type IF health < 50 THEN use_potion).
  • Formats de requête simples (comme des query strings interprétées).

Inconvénients majeurs

  1. Maintenance de la grammaire : Ajouter une nouvelle règle implique de créer de nouvelles classes et souvent de modifier le parser.
  2. Complexité : Pour des grammaires un peu riches, le nombre de classes explose.
  3. Performance : L’interprétation pas-à-pas d’un AST est plus lente que l’exécution de code compilé ou d’un eval natif (si disponible et sécurisé).
  4. Parser séparé : Le pattern Interpreter lui-même ne définit pas comment parser la phrase pour construire l’AST. Il faut coder un parser séparé, ce qui est la partie difficile.

Alternatives

  • Évaluer avec eval() : Si le langage est un sous-ensemble sûr de JavaScript, eval() ou Function peuvent être plus simples. Mais DANGEREUX si non contrôlé.
  • Utiliser une bibliothèque d’expressions (comme expr-eval pour JS).
  • Générateurs de parsers (ANTLR, Peg.js) pour des grammaires plus complexes.

Le transmetteur morse était silencieux. L’Interpreter était l’outil du déchiffreur, du traducteur de micro-langages. Un pattern rare, spécialisé, presque ésotérique. Mais quand on tombait sur un problème de langue restreinte à traduire en actions, il n’y avait pas plus élégant.

C’était le dernier. Le 23ème et dernier pattern du Gang of Four.

La boucle était bouclée. Du chaos initial aux patterns de création, de structure et de comportement, on avait parcouru l’arsenal du développeur pour dompter la complexité.

Il ne restait plus qu’à tirer les conclusions, à faire la synthèse de ce voyage dans l’ombre du code. Le prochain chapitre serait la conclusion.