Skip to content
SP StackPractices
advanced

Patrón Interpreter

Define una representación para la gramática de un lenguaje junto con un intérprete que usa la representación para interpretar oraciones. Un patrón de diseño de comportamiento para mini-lenguajes.

Temas: design

Patrón Interpreter

Resumen

El Patrón Interpreter es un patrón de diseño de comportamiento que define una representación para la gramática de un lenguaje junto con un intérprete que usa la representación para interpretar oraciones en el lenguaje. Es ideal para construir lenguajes específicos de dominio (DSL), evaluadores de expresiones, analizadores de consultas y motores de reglas.

Cuándo usarlo

Usa el Patrón Interpreter cuando:

  • Tengas una gramática simple que necesita ser analizada y evaluada frecuentemente
  • Quieras construir un lenguaje específico de dominio (DSL) para configuración o reglas
  • Las oraciones del lenguaje puedan representarse como árboles de sintaxis abstracta (AST)
  • La simplicidad de la gramática hace que un generador de parsers (ANTLR, yacc) sea excesivo
  • Ejemplos: expresiones aritméticas, consultas tipo SQL, motores de reglas booleanas, motores regex

Solución

Python

from abc import ABC, abstractmethod
from typing import Any

class Expression(ABC):
    @abstractmethod
    def interpret(self, context: dict) -> Any:
        pass

class Number(Expression):
    def __init__(self, value: float):
        self.value = value

    def interpret(self, context: dict) -> float:
        return self.value

class Variable(Expression):
    def __init__(self, name: str):
        self.name = name

    def interpret(self, context: dict) -> Any:
        return context.get(self.name, 0)

class Add(Expression):
    def __init__(self, left: Expression, right: Expression):
        self.left = left
        self.right = right

    def interpret(self, context: dict) -> float:
        return self.left.interpret(context) + self.right.interpret(context)

class Subtract(Expression):
    def __init__(self, left: Expression, right: Expression):
        self.left = left
        self.right = right

    def interpret(self, context: dict) -> float:
        return self.left.interpret(context) - self.right.interpret(context)

# Uso: evaluar "(10 + x) - 5" donde x = 3
expression = Subtract(
    Add(Number(10), Variable("x")),
    Number(5)
)
context = {"x": 3}
print(expression.interpret(context))  # 8

JavaScript

class Expression {
  interpret(context) {
    throw new Error("Las subclases deben implementar interpret()");
  }
}

class NumberLiteral extends Expression {
  constructor(value) {
    super();
    this.value = value;
  }

  interpret(context) {
    return this.value;
  }
}

class Variable extends Expression {
  constructor(name) {
    super();
    this.name = name;
  }

  interpret(context) {
    return context[this.name] ?? 0;
  }
}

class Add extends Expression {
  constructor(left, right) {
    super();
    this.left = left;
    this.right = right;
  }

  interpret(context) {
    return this.left.interpret(context) + this.right.interpret(context);
  }
}

class Subtract extends Expression {
  constructor(left, right) {
    super();
    this.left = left;
    this.right = right;
  }

  interpret(context) {
    return this.left.interpret(context) - this.right.interpret(context);
  }
}

// Uso: evaluar "(10 + x) - 5" donde x = 3
const expression = new Subtract(
  new Add(new NumberLiteral(10), new Variable("x")),
  new NumberLiteral(5)
);
console.log(expression.interpret({ x: 3 })); // 8

Java

import java.util.Map;

public interface Expression {
    double interpret(Map<String, Double> context);
}

public class NumberLiteral implements Expression {
    private final double value;

    public NumberLiteral(double value) {
        this.value = value;
    }

    public double interpret(Map<String, Double> context) {
        return value;
    }
}

public class Variable implements Expression {
    private final String name;

    public Variable(String name) {
        this.name = name;
    }

    public double interpret(Map<String, Double> context) {
        return context.getOrDefault(name, 0.0);
    }
}

public class Add implements Expression {
    private final Expression left, right;

    public Add(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    public double interpret(Map<String, Double> context) {
        return left.interpret(context) + right.interpret(context);
    }
}

public class Subtract implements Expression {
    private final Expression left, right;

    public Subtract(Expression left, Expression right) {
        this.left = left;
        this.right = right;
    }

    public double interpret(Map<String, Double> context) {
        return left.interpret(context) - right.interpret(context);
    }
}

// Uso
Expression expr = new Subtract(
    new Add(new NumberLiteral(10), new Variable("x")),
    new NumberLiteral(5)
);
System.out.println(expr.interpret(Map.of("x", 3.0))); // 8.0

Explicación

El Patrón Interpreter se construye alrededor de un árbol de sintaxis abstracta (AST):

  • Expresión Abstracta (Expression): Declara la interfaz interpret()
  • Expresión Terminal (NumberLiteral, Variable): Representa nodos hoja en el AST
  • Expresión No Terminal (Add, Subtract): Representa nodos compuestos que combinan otras expresiones
  • Contexto (dict/Map): Mantiene estado global (variables) disponible durante la interpretación

El cliente construye un AST y luego llama interpret() en el nodo raíz, que evalúa recursivamente el árbol.

Variantes

VarianteDescripciónCaso de uso
Tree WalkerRecorre un AST pre-construidoMotores de reglas, evaluadores de consultas
Visitor + InterpreterSepara la lógica de evaluación de los nodos del árbolCuando las estrategias de evaluación varían
Interpreter MemoizadoCachea resultados de sub-expresionesEvaluaciones repetidas con el mismo contexto
Compiler + VMCompila a bytecode, luego ejecutaEjecución repetida de alto rendimiento

Mejores prácticas

  • Mantén la gramática simple — las gramáticas complejas se manejan mejor con generadores de parsers
  • Construye el AST primero, luego interpreta — no interpretes durante el parsing
  • Usa nodos de expresión inmutables para seguridad de hilos y reproducibilidad
  • Documenta la precedencia de operadores claramente si soportas múltiples operadores
  • Considera agregar toString() para depuración y serialización de expresiones

Errores comunes

  • Usar Interpreter para lenguajes complejos donde un generador de parsers (ANTLR, PEG.js) es más apropiado
  • Mezclar la lógica de parsing con la lógica de interpretación, creando código spaghetti
  • No manejar errores de tipo o variables faltantes gracefulmente
  • Construir el AST manualmente en lugar de usar un parser para gramáticas más grandes
  • Ignorar la precedencia de operadores, llevando a orden de evaluación incorrecto

Preguntas frecuentes

P: ¿Cuándo debería usar Interpreter en lugar de un generador de parsers? R: Usa Interpreter para gramáticas muy simples con pocas reglas que cambian infrecuentemente. Para cualquier cosa más allá de aritmética básica o expresiones booleanas, usa ANTLR, PEG.js o una herramienta similar.

P: ¿Puedo combinar Interpreter con Visitor? R: Sí, y a menudo deberías. El Patrón Interpreter define la estructura del AST; el Patrón Visitor agrega operaciones (interpretar, serializar, optimizar) sin modificar los nodos del AST.