Skip to content
BBaskaran

Interpreter

Represent the grammar of a small language as classes, so its sentences can be evaluated.

Use it when

You need to evaluate simple rules or expressions, such as filters, formulas or discount conditions.

/**
 * Interpreter: evaluate simple discount rules like "total > 100 and member".
 *
 * Real-world scenario: a shop lets the marketing team define discount rules
 * without changing code. Each part of a rule is a small class: constants and
 * variables are the leaves, and "greater than" and "and" combine them. A rule is
 * a tree of these objects, and evaluating it means asking the root to interpret
 * itself against a customer's cart.
 *
 * The rule trees are built by hand here; a real app would parse rule text into
 * the same objects.
 */

type Value = number | boolean;
type Context = Record<string, Value>;

// Expression: every part of a rule can interpret itself against a context.
interface Expression {
  interpret(context: Context): Value;
  toString(): string;
}

// Terminal expressions: the leaves of the tree.
// (Named Constant because Number is a built-in JavaScript type.)
class Constant implements Expression {
  private readonly value: number;

  constructor(value: number) {
    this.value = value;
  }

  interpret(): Value {
    return this.value;
  }

  toString(): string {
    return String(this.value);
  }
}

class Variable implements Expression {
  private readonly name: string;

  constructor(name: string) {
    this.name = name;
  }

  interpret(context: Context): Value {
    return context[this.name];
  }

  toString(): string {
    return this.name;
  }
}

// Non-terminal expressions: combine other expressions.
class GreaterThan implements Expression {
  private readonly left: Expression;
  private readonly right: Expression;

  constructor(left: Expression, right: Expression) {
    this.left = left;
    this.right = right;
  }

  interpret(context: Context): Value {
    return Number(this.left.interpret(context)) > Number(this.right.interpret(context));
  }

  toString(): string {
    return `${this.left} > ${this.right}`;
  }
}

class And implements Expression {
  private readonly left: Expression;
  private readonly right: Expression;

  constructor(left: Expression, right: Expression) {
    this.left = left;
    this.right = right;
  }

  interpret(context: Context): Value {
    return Boolean(this.left.interpret(context)) && Boolean(this.right.interpret(context));
  }

  toString(): string {
    return `${this.left} and ${this.right}`;
  }
}

function clientCode(): void {
  // Two rules built from the same small set of classes.
  const memberDeal = new And(
    new GreaterThan(new Variable("total"), new Constant(100)),
    new Variable("member"),
  );
  const bulkDeal = new GreaterThan(new Variable("items"), new Constant(5));

  const carts: Record<string, Context> = {
    Asha: { total: 150, items: 2, member: true },
    Ravi: { total: 150, items: 8, member: false },
    Meena: { total: 80, items: 3, member: true },
  };

  for (const rule of [memberDeal, bulkDeal]) {
    console.log(`Rule: ${rule}`);
    for (const [customer, cart] of Object.entries(carts)) {
      const result = rule.interpret(cart) ? "gets discount" : "no discount";
      console.log(`  ${customer}: ${result}`);
    }
  }
}

clientCode();

// Output:
// Rule: total > 100 and member
//   Asha: gets discount
//   Ravi: no discount
//   Meena: no discount
// Rule: items > 5
//   Asha: no discount
//   Ravi: gets discount
//   Meena: no discount

How it works

RoleClassesWhat it does
Abstract expressionExpressionInterface: every part of a rule can interpret() itself against a context
Terminal expressionsConstant, VariableThe leaves: a fixed number, or a value looked up in the cart
Non-terminal expressionsGreaterThan, AndCombine other expressions; nesting them builds a whole rule
ContextContext (Record)The customer's data the rule is evaluated against
ClientclientCode()Builds rule trees and evaluates them for each customer