Skip to content
StackPractices
advanced Por Mathias Paulenko

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

Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.

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

Lo que funciona

  • 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.

¿Es este patrón adecuado para proyectos pequeños?

Para proyectos pequeños con pocos componentes, este patrón puede añadir complejidad innecesaria. Empieza simple e introduce el patrón cuando sientas el problema que resuelve.

¿Cómo se compara este patrón con alternativas?

Cada patrón hace diferentes trade-offs. Revisa la tabla de variantes arriba y considera tus restricciones específicas: tamaño del equipo, requisitos de rendimiento y planes de escalado.

¿Puedo aplicar este patrón parcialmente?

Sí. Muchos equipos adoptan patrones incrementalmente. Empieza con la idea central y añade sofisticación según sea necesario. El patrón es una guía, no un blueprint estricto.

Temas Avanzados

Escenario: Interpreter para DSL de Configuracion

// Interpreter: evaluar expresiones de un DSL
// Gramatica: SET key value | GET key | DEL key | EXISTS key

interface Expression {
  interpret(context: Map<string, string>): string;
}

// Terminal: SET
class SetExpression implements Expression {
  constructor(private key: string, private value: string) {}
  interpret(context: Map<string, string>): string {
    context.set(this.key, this.value);
    return `OK: ${this.key} = ${this.value}`;
  }
}

// Terminal: GET
class GetExpression implements Expression {
  constructor(private key: string) {}
  interpret(context: Map<string, string>): string {
    const value = context.get(this.key);
    return value !== undefined ? value : `NIL: ${this.key}`;
  }
}

// Terminal: DEL
class DelExpression implements Expression {
  constructor(private key: string) {}
  interpret(context: Map<string, string>): string {
    const existed = context.delete(this.key);
    return existed ? `OK: ${this.key} deleted` : `NIL: ${this.key}`;
  }
}

// Non-terminal: secuencia de expresiones
class SequenceExpression implements Expression {
  constructor(private expressions: Expression[]) {}
  interpret(context: Map<string, string>): string {
    return this.expressions.map(e => e.interpret(context)).join("\n");
  }
}

// Parser: convertir texto a arbol de expresiones
class ConfigParser {
  parse(input: string): Expression {
    const lines = input.trim().split("\n");
    const expressions: Expression[] = [];
    for (const line of lines) {
      const parts = line.split(" ");
      const cmd = parts[0].toUpperCase();
      if (cmd === "SET") expressions.push(new SetExpression(parts[1], parts.slice(2).join(" ")));
      else if (cmd === "GET") expressions.push(new GetExpression(parts[1]));
      else if (cmd === "DEL") expressions.push(new DelExpression(parts[1]));
      else throw new Error(`Unknown command: ${cmd}`);
    }
    return new SequenceExpression(expressions);
  }
}

// Uso
const parser = new ConfigParser();
const context = new Map<string, string>();
const program = parser.parse("SET name Alice\nSET role admin\nGET name\nDEL role");
console.log(program.interpret(context));
// OK: name = Alice
// OK: role = admin
// Alice
// OK: role deleted

Lecciones:

  • Interpreter evalua expresiones de un DSL (Domain Specific Language)
  • Cada expresion implementa interpret(): patron recursivo
  • Terminal: SET, GET, DEL. Non-terminal: Sequence (compone)
  • El parser convierte texto en arbol de expresiones
  • Ideal para configs, queries, reglas de negocio simples
  • Para DSLs complejos, usar parser generators (ANTLR, nearley)

### Interpreter vs Visitor: cual uso?

Interpreter evalua un arbol de expresiones: cada nodo sabe como interpretarse. Visitor recorre un arbol de objetos: el visitor sabe que hacer con cada nodo. Interpreter es para ejecutar un DSL. Visitor es para operaciones sobre una estructura. Usa Interpreter cuando necesitas un lenguaje custom (config, query, rules). Usa Visitor cuando necesitas anadir operaciones a una estructura sin modificar las clases.