Interpreter Pattern Guide
This guide covers everything you need to know about using the Interpreter pattern in PatternKit.
Overview
The Interpreter pattern lets you define a grammar for a language and provide an interpreter that evaluates sentences in that language. It's ideal for:
- Building domain-specific languages (DSLs)
- Evaluating mathematical expressions
- Implementing business rule engines
- Creating query/filter languages
Getting Started
Installation
The Interpreter pattern is included in the core PatternKit package:
using PatternKit.Behavioral.Interpreter;
using static PatternKit.Behavioral.Interpreter.ExpressionExtensions;
Basic Usage
Create an interpreter in three steps:
// 1. Create the builder
var interpreter = Interpreter.Create<MyContext, double>()
// 2. Register expression handlers
.Terminal("number", token => double.Parse(token))
.Binary("add", (left, right) => left + right)
// 3. Build the immutable interpreter
.Build();
// 4. Evaluate expressions
var expr = NonTerminal("add",
Terminal("number", "5"),
Terminal("number", "3"));
double result = interpreter.Interpret(expr); // 8.0
Core Concepts
Terminal Expressions
Terminal expressions are leaf nodes that produce values from literal tokens. They don't have children.
// Simple terminal - parse a number
.Terminal("number", token => double.Parse(token))
// Terminal with context - read a variable
.Terminal("var", (token, ctx) => ctx.Variables[token])
// Terminal with validation
.Terminal("positive", token =>
{
var value = double.Parse(token);
if (value < 0) throw new ArgumentException("Must be positive");
return value;
})
Non-Terminal Expressions
Non-terminal expressions combine child results to produce new values.
// Binary operation (exactly 2 children)
.Binary("add", (left, right) => left + right)
// Unary operation (exactly 1 child)
.Unary("negate", value => -value)
// General non-terminal (any number of children)
.NonTerminal("sum", args => args.Sum())
// Conditional (3 children: condition, then, else)
.NonTerminal("if", (args, _) =>
args[0] > 0 ? args[1] : args[2])
Context
Context provides state during interpretation. Use it for variables, configuration, or external data.
public class PricingContext
{
public decimal CartTotal { get; set; }
public string CustomerTier { get; set; }
public Dictionary<string, decimal> Variables { get; set; }
}
var interpreter = Interpreter.Create<PricingContext, decimal>()
.Terminal("cart", (_, ctx) => ctx.CartTotal)
.Terminal("var", (name, ctx) => ctx.Variables[name])
.Build();
var context = new PricingContext { CartTotal = 100m };
var result = interpreter.Interpret(expr, context);
Building Expressions
PatternKit provides helper methods for building expression trees:
using static PatternKit.Behavioral.Interpreter.ExpressionExtensions;
// Terminal expressions
var num = Terminal("number", "42");
var str = Terminal("string", "hello");
var id = Terminal("identifier", "x");
// Convenience methods
var n = Number(42); // Terminal("number", "42")
var s = String("hello"); // Terminal("string", "hello")
var b = Boolean(true); // Terminal("boolean", "true")
var x = Identifier("x"); // Terminal("identifier", "x")
// Non-terminal expressions
var add = NonTerminal("add", Number(1), Number(2));
var nested = NonTerminal("mul",
NonTerminal("add", Number(1), Number(2)),
Number(3));
Async Interpreters
Use AsyncInterpreter when expressions need to perform async operations:
var asyncInterpreter = AsyncInterpreter.Create<Context, decimal>()
// Sync terminal (still works)
.Terminal("number", token => decimal.Parse(token))
// Async terminal - database lookup
.Terminal("price", async (sku, ctx, ct) =>
{
return await db.GetPriceAsync(sku, ct);
})
// Async non-terminal
.NonTerminal("discount", async (args, ctx, ct) =>
{
var rate = await GetDiscountRateAsync(ctx.CustomerId, ct);
return args[0] * rate;
})
.Build();
// Evaluate asynchronously
var result = await asyncInterpreter.InterpretAsync(expr, context);
Common Patterns
Arithmetic Calculator
var calc = Interpreter.Create<object, double>()
.Terminal("number", token => double.Parse(token))
.Binary("add", (l, r) => l + r)
.Binary("sub", (l, r) => l - r)
.Binary("mul", (l, r) => l * r)
.Binary("div", (l, r) => r != 0 ? l / r : throw new DivideByZeroException())
.Unary("neg", v => -v)
.Unary("abs", Math.Abs)
.Build();
Boolean Logic
var logic = Interpreter.Create<Context, bool>()
.Terminal("true", _ => true)
.Terminal("false", _ => false)
.Terminal("var", (name, ctx) => ctx.GetBool(name))
.Binary("and", (l, r) => l && r)
.Binary("or", (l, r) => l || r)
.Unary("not", v => !v)
.Build();
Business Rules Engine
var rules = Interpreter.Create<OrderContext, decimal>()
// Variables
.Terminal("var", (name, ctx) => name switch
{
"subtotal" => ctx.Subtotal,
"item_count" => ctx.Items.Count,
"tier_discount" => ctx.Customer.TierDiscount,
_ => ctx.Variables.GetValueOrDefault(name, 0m)
})
// Arithmetic
.Binary("add", (l, r) => l + r)
.Binary("mul", (l, r) => l * r)
.Binary("min", Math.Min)
.Binary("max", Math.Max)
// Comparisons (return 1 for true, 0 for false)
.Binary("gt", (l, r) => l > r ? 1m : 0m)
.Binary("gte", (l, r) => l >= r ? 1m : 0m)
// Conditional
.NonTerminal("if", (args, _) =>
args[0] > 0 ? args[1] : args[2])
.Build();
Extending the Pattern
Custom Expression Types
You can create custom expression classes for domain-specific needs:
public class DiscountExpression : IExpression
{
public string Type => "discount";
public string DiscountCode { get; }
public IExpression Amount { get; }
public DiscountExpression(string code, IExpression amount)
{
DiscountCode = code;
Amount = amount;
}
}
Expression Builders
Create fluent builders for complex expressions:
public static class RuleBuilder
{
public static IExpression PercentOff(decimal percent, IExpression baseAmount)
=> NonTerminal("mul", baseAmount, Terminal("number", (percent / 100).ToString()));
public static IExpression IfOver(decimal threshold, IExpression thenExpr, IExpression elseExpr)
=> NonTerminal("if",
NonTerminal("gt", Terminal("var", "subtotal"), Terminal("number", threshold.ToString())),
thenExpr,
elseExpr);
}
// Usage
var rule = RuleBuilder.IfOver(100m,
RuleBuilder.PercentOff(10m, Terminal("var", "subtotal")),
Terminal("number", "0"));
Combining with Other Patterns
With Factory
Use Factory to create interpreters based on configuration:
var interpreterFactory = Factory<string, Interpreter<Context, decimal>>.Create()
.Map("pricing", () => CreatePricingInterpreter())
.Map("tax", () => CreateTaxInterpreter())
.Map("shipping", () => CreateShippingInterpreter())
.Build();
var interpreter = interpreterFactory.Create("pricing");
With Strategy
Use Strategy to select different evaluation strategies:
var evaluator = Strategy<EvalRequest, decimal>.Create()
.When(req => req.Type == "pricing")
.Then(req => pricingInterpreter.Interpret(req.Expression, req.Context))
.When(req => req.Type == "tax")
.Then(req => taxInterpreter.Interpret(req.Expression, req.Context))
.Default(req => 0m)
.Build();
With Chain
Use Chain for multi-stage interpretation:
var pipeline = ResultChain<EvalContext, decimal>.Create()
.When(ctx => ctx.HasDiscounts)
.Then(ctx => discountInterpreter.Interpret(ctx.DiscountRule, ctx))
.When(ctx => ctx.HasTax)
.Then(ctx => taxInterpreter.Interpret(ctx.TaxRule, ctx))
.Finally((ctx, _) => ctx.BaseAmount)
.Build();
Best Practices
Keep grammars simple: Complex grammars are hard to maintain. Consider parser generators for complex languages.
Validate expressions early: Check expression structure before interpretation to provide clear error messages.
Use context sparingly: Pass only what's needed. Large contexts slow down evaluation.
Cache interpreters: Build once, reuse many times. Interpreters are immutable and thread-safe.
Prefer binary/unary helpers: They validate operand counts and provide better error messages.
Document your grammar: Create a grammar specification for users of your DSL.
Troubleshooting
"No terminal handler registered for 'xyz'"
You're trying to interpret an expression type that wasn't registered:
// Wrong: using "num" when "number" was registered
Terminal("num", "42")
// Right: use the registered name
Terminal("number", "42")
"Binary operator requires exactly 2 operands"
Your expression has the wrong number of children:
// Wrong: 3 children for a binary operator
NonTerminal("add", Number(1), Number(2), Number(3))
// Right: use a general non-terminal for n-ary operations
.NonTerminal("sum", args => args.Sum())
Stack overflow with recursive expressions
Be careful with self-referential expressions. Add depth limits:
.NonTerminal("recurse", (args, ctx) =>
{
if (ctx.Depth++ > 100)
throw new InvalidOperationException("Max recursion depth exceeded");
return Interpret(args[0], ctx);
})
FAQ
Q: Can I modify an interpreter after building? A: No. Interpreters are immutable. Create a new one if you need different behavior.
Q: How do I handle errors gracefully?
A: Use TryInterpret which returns false instead of throwing:
if (interpreter.TryInterpret(expr, context, out var result))
Console.WriteLine($"Result: {result}");
else
Console.WriteLine("Interpretation failed");
Q: Can I serialize expressions? A: Yes. Expressions are plain objects. Use your preferred serialization (JSON, etc.) and rebuild the tree on deserialization.
Q: What's the performance overhead? A: Each expression node involves a dictionary lookup and delegate invocation. For hot paths, consider compiling expressions to IL or caching results.