C# Interpreter Pattern

Summary: in this tutorial, you will learn how to use the C# interpreter pattern to build grammar for interpreting a simple language.

Introduction to the C# interpreter pattern

The interpreter pattern is a behavior pattern that allows you to build an interpreter for a simple language and specifies how to evaluate sentences or expressions in that language.

The interpreter pattern defines the grammar of the language and provides a way to evaluate the grammar.

Typically, the grammar consists of a set of classes, each of which represents a rule in the grammar. The interpreter pattern combines the rules to form a hierarchy with the most basic rules at the bottom and the more complex rules at the top,

For example, the following picture illustrates an expression 10 + 20 * (2 + 3) as a tree of rules:

C# Interpreter Tree Structure

To evaluate a sentence or expression, the interpreter takes input and applies the grammar rules to it. More specifically, the interpreter recursively evaluates the rules, applies the most basic rules first, and combines the result of each evaluation until the entire sentence or expression is evaluated.

The result of the evaluation is typically a value or a set of values, depending on the specific application of the interpreter.

The following UML diagram illustrates the Interpreter pattern:

C# Interpreter Pattern

The interpreter pattern involves the following participants:

  • AbstractExpression is an abstract class or an interface that has interpret() method for evaluating the expression.
  • TerminalExpression is a concrete implementation of the abstract expression. The TerminalExpression represents the simplest elements of the grammar such as variables, constants, or literals. A terminal expression cannot be further decomposed into smaller expressions. For example, in an arithmetic expression grammar, a terminal expression could be a number or a variable.
  • NonTerminalExpression represents the more complex elements of the language grammar built from simpler expressions. Unlike a terminal expression, a non-terminal expression can be decomposed into smaller expressions until they reach terminal expressions. For example, in an arithmetic expression grammar, a non-terminal expression could represent a binary operation such as addition, which is built from two terminal expressions.
  • Context is the context in that the interpreter evaluates the expression. It contains the information required for the evaluation of the expression.
  • Client is a class that is responsible for building an expression using the TerminalExpression and NonterminalExpression. It passes the expression to the interpreter for evaluation.

For example, the following picture shows the terminal and non-terminal expressions:

C# Interpreter - TerminalExpression and NonTerminal Expression

C# interpreter pattern example

The following program demonstrates how to use the C# interpreter pattern:

namespace IterpreterPattern;

// Abstract Expression
public abstract class Expression
{
    public abstract int Interpret(Context context);
}

// Terminal Expression
public class NumberExpression : Expression
{
    private readonly int number;
    public NumberExpression(int number)
    {
        this.number = number;
    }
    public override int Interpret(Context context)
    {
        return number;
    }
}

// Non-terminal Expression
public  class AddExpression : Expression
{
    private readonly Expression _left;
    private readonly Expression _right;
    public AddExpression(Expression leftExpression, Expression rightExpression)
    {
        _left = leftExpression;
        _right = rightExpression;
    }
    public override int Interpret(Context context)
    {
        return _left.Interpret(context) + _right.Interpret(context);
    }
}
public class SubtractExpression : Expression
{
    private readonly Expression _left;
    private readonly Expression _right;
    public SubtractExpression(Expression leftExpression, Expression rightExpression)
    {
        _left = leftExpression;
        _right = rightExpression;
    }
    public override int Interpret(Context context)
    {
        return _left.Interpret(context) - _right.Interpret(context);
    }
}

public class MultiplyExpression : Expression
{
    private readonly Expression _left;
    private readonly Expression _right;
    public MultiplyExpression(Expression leftExpression, Expression rightExpression)
    {
        _left = leftExpression;
        _right = rightExpression;
    }
    public override int Interpret(Context context)
    {
        return _left.Interpret(context) * _right.Interpret(context);
    }
}
public class DivideExpression : Expression
{
    private readonly Expression _left;
    private readonly Expression _right;
    public DivideExpression(Expression leftExpression, Expression rightExpression)
    {
        _left = leftExpression;
        _right = rightExpression;
    }
    public override int Interpret(Context context)
    {
        return _left.Interpret(context) / _right.Interpret(context);
    }
}

// Context
public class Context
{
    private readonly Dictionary<string, int> _variables;

    public Context()
    {
        _variables = new Dictionary<string, int>();
    }

    public void SetVariable(string name, int value)
    {
        _variables[name] = value;
    }

    public int GetVariable(string name)
    {
        if (_variables.TryGetValue(name, out var value))
        {
            return value;
        }
        else
        {
            throw new ArgumentException($"Variable '{name}' not found.");
        }
    }
}

// Client
public class Client
{
    public static void Main(string[] args)
    {
        var context = new Context();
        context.SetVariable("x", 2);
        context.SetVariable("y", 3);

        // -> 10 + 20*(x + y)
        var expression = new AddExpression(
            new NumberExpression(10),
            new MultiplyExpression(  
                new NumberExpression(20),
                new AddExpression(
                    new NumberExpression(context.GetVariable("x")),
                    new NumberExpression(context.GetVariable("y"))
                )
            )
        );

        var result = expression.Interpret(context);

        Console.WriteLine("Result: " + result); 
    }
}Code language: C# (cs)

Output:

Result: 110Code language: C# (cs)

How it works.

First, define the abstract class Expression:

public abstract class Expression
{
    public abstract int Interpret(Context context);
}Code language: C# (cs)

Second, define the NumberExpression class that inherits from the Expression class:

public class NumberExpression : Expression
{
    private readonly int number;
    public NumberExpression(int number)
    {
        this.number = number;
    }
    public override int Interpret(Context context)
    {
        return number;
    }
}Code language: C# (cs)

The NumberExpression serves as a TerminalExpression.

Third, define the AddExpression class that sums up two expressions:

public class AddExpression : Expression
{
    private readonly Expression _left;
    private readonly Expression _right;
    public AddExpression(Expression leftExpression, Expression rightExpression)
    {
        _left = leftExpression;
        _right = rightExpression;
    }
    public override int Interpret(Context context)
    {
        return _left.Interpret(context) + _right.Interpret(context);
    }
}Code language: C# (cs)

The AddExpression serves as a NonterminalExpression.

Fourth, define the SubtractExpression, MultiplyExpression, and DivideExpression classes like the AddExpression class but subtract, multiply, and divide two numbers respectively.

public class SubtractExpression : Expression
{
    private readonly Expression _left;
    private readonly Expression _right;
    public SubtractExpression(Expression leftExpression, Expression rightExpression)
    {
        _left = leftExpression;
        _right = rightExpression;
    }
    public override int Interpret(Context context)
    {
        return _left.Interpret(context) - _right.Interpret(context);
    }
}

public class MultiplyExpression : Expression
{
    private readonly Expression _left;
    private readonly Expression _right;
    public MultiplyExpression(Expression leftExpression, Expression rightExpression)
    {
        _left = leftExpression;
        _right = rightExpression;
    }
    public override int Interpret(Context context)
    {
        return _left.Interpret(context) * _right.Interpret(context);
    }
}
public class DivideExpression : Expression
{
    private readonly Expression _left;
    private readonly Expression _right;
    public DivideExpression(Expression leftExpression, Expression rightExpression)
    {
        _left = leftExpression;
        _right = rightExpression;
    }
    public override int Interpret(Context context)
    {
        return _left.Interpret(context) / _right.Interpret(context);
    }
}Code language: C# (cs)

Fifth, define the Context class that maintains a dictionary of variables for the expression:

public class Context
{
    private readonly Dictionary<string, int> _variables;

    public Context()
    {
        _variables = new Dictionary<string, int>();
    }

    public void SetVariable(string name, int value)
    {
        _variables[name] = value;
    }

    public int GetVariable(string name)
    {
        if (_variables.TryGetValue(name, out var value))
        {
            return value;
        }
        else
        {
            throw new ArgumentException($"Variable '{name}' not found.");
        }
    }
}Code language: C# (cs)

Finally, define the Client that composes the expression: 10 + 20 * (x + y), where x is 2 and y is 3:

public class Client
{
    public static void Main(string[] args)
    {
        var context = new Context();
        context.SetVariable("x", 2);
        context.SetVariable("y", 3);

        // -> 10 + 20*(x + y)
        var expression = new AddExpression(
            new NumberExpression(10),
            new MultiplyExpression(  
                new NumberExpression(20),
                new AddExpression(
                    new NumberExpression(context.GetVariable("x")),
                    new NumberExpression(context.GetVariable("y"))
                )
            )
        );

        var result = expression.Interpret(context);

        Console.WriteLine("Result: " + result); 
    }
}Code language: C# (cs)

Summary

  • Use the interpreter pattern to build an interpreter for a simple language.
Was this tutorial helpful ?