Learnixo

.NET & C# Development · Lesson 45 of 229

Interpreter — Grammar as Code

Interpreter — Grammar as Code

The Interpreter pattern defines a representation for a grammar and provides an interpreter to process sentences in that grammar. It turns a domain language into an executable class hierarchy.


Mathematical Expression Evaluator

C#
// Abstract expression — every node in the syntax tree
public interface IExpression
{
    double Evaluate(Dictionary<string, double> context);
}

// Terminal: a literal number
public class NumberExpression(double value) : IExpression
{
    public double Evaluate(Dictionary<string, double> _) => value;
}

// Terminal: a variable reference
public class VariableExpression(string name) : IExpression
{
    public double Evaluate(Dictionary<string, double> ctx)
        => ctx.TryGetValue(name, out var val) ? val
            : throw new KeyNotFoundException($"Variable '{name}' not defined");
}

// Non-terminal: binary operations
public class AddExpression(IExpression left, IExpression right) : IExpression
{
    public double Evaluate(Dictionary<string, double> ctx)
        => left.Evaluate(ctx) + right.Evaluate(ctx);
}

public class MultiplyExpression(IExpression left, IExpression right) : IExpression
{
    public double Evaluate(Dictionary<string, double> ctx)
        => left.Evaluate(ctx) * right.Evaluate(ctx);
}

public class SubtractExpression(IExpression left, IExpression right) : IExpression
{
    public double Evaluate(Dictionary<string, double> ctx)
        => left.Evaluate(ctx) - right.Evaluate(ctx);
}

// Build expression tree for: (a + b) * (c - 2)
var context = new Dictionary<string, double> { ["a"] = 3, ["b"] = 7, ["c"] = 5 };

IExpression expr = new MultiplyExpression(
    new AddExpression(
        new VariableExpression("a"),
        new VariableExpression("b")
    ),
    new SubtractExpression(
        new VariableExpression("c"),
        new NumberExpression(2)
    )
);

Console.WriteLine(expr.Evaluate(context));   // (3+7) * (5-2) = 30

Business Rule Engine

C#
// Boolean expression interpreter for eligibility rules
public interface IRule<T>
{
    bool IsSatisfied(T subject);
    string Description { get; }
}

public class MinimumAgeRule(int minAge) : IRule<Customer>
{
    public string Description => $"Age >= {minAge}";
    public bool IsSatisfied(Customer c) => c.Age >= minAge;
}

public class MinimumSpendRule(decimal threshold) : IRule<Customer>
{
    public string Description => $"Total spend >= £{threshold}";
    public bool IsSatisfied(Customer c) => c.TotalSpend >= threshold;
}

public class AndRule<T>(IRule<T> left, IRule<T> right) : IRule<T>
{
    public string Description => $"({left.Description} AND {right.Description})";
    public bool IsSatisfied(T s) => left.IsSatisfied(s) && right.IsSatisfied(s);
}

public class OrRule<T>(IRule<T> left, IRule<T> right) : IRule<T>
{
    public string Description => $"({left.Description} OR {right.Description})";
    public bool IsSatisfied(T s) => left.IsSatisfied(s) || right.IsSatisfied(s);
}

public class IsVipRule : IRule<Customer>
{
    public string Description => "Is VIP";
    public bool IsSatisfied(Customer c) => c.IsVip;
}

// (age >= 18 AND spend >= 500) OR is a VIP
IRule<Customer> rule = new OrRule<Customer>(
    new AndRule<Customer>(
        new MinimumAgeRule(18),
        new MinimumSpendRule(500m)
    ),
    new IsVipRule()
);

var customer = new Customer { Age = 25, TotalSpend = 600m, IsVip = false };
Console.WriteLine(rule.Description);
// ((Age >= 18 AND Total spend >= £500) OR Is VIP)
Console.WriteLine(rule.IsSatisfied(customer));   // True

Simple Filter DSL

C#
// DSL: "status:Paid AND total:>100"
public static class FilterParser
{
    public static Func<Order, bool> Parse(string filter)
    {
        var parts = filter.Split(" AND ");
        var predicates = parts.Select(ParseClause).ToList();
        return order => predicates.All(p => p(order));
    }

    private static Func<Order, bool> ParseClause(string clause)
    {
        var parts = clause.Trim().Split(':', 2);
        return (parts[0], parts[1]) switch
        {
            ("status", var val)     => o => o.Status == val,
            ("total",  var val) when val.StartsWith(">")
                => o => o.Total > decimal.Parse(val[1..]),
            ("total",  var val) when val.StartsWith("<")
                => o => o.Total < decimal.Parse(val[1..]),
            (var field, _) => throw new ArgumentException($"Unknown filter: {field}")
        };
    }
}

When to Use

Use Interpreter when:
  ✓ Defining a small DSL for business rules, queries, or formulas
  ✓ Rules need to be composed at runtime (AND, OR, NOT)
  ✓ Non-developers need to configure eligibility or pricing rules

Avoid when:
  ✗ Grammar is complex — use a parser library (Sprache, ANTLR)
  ✗ Simple one-off conditionals — just write the if statement

Interview Answer

"The Interpreter pattern represents a grammar as a class hierarchy — terminal classes handle literals and variables, non-terminal classes handle operations (AND, OR, +, *). Sentences in the language are built as object trees (composite) and evaluated by calling a common method. Common uses: business rule engines (eligibility checks composed from individual rules), expression evaluators, and DSL parsers for filtering or querying. LINQ expression trees are the built-in .NET implementation — EF Core interprets them to generate SQL. The trade-off: works well for simple grammars; becomes unmanageable for complex languages — use a proper parser generator for those."