What is the Interpreter Design Pattern?

Göksu Deniz
10 min readDec 20, 2022

--

Image is created by DALL-E 2 AI

The interpreter design pattern is a behavioral design pattern that is used to define a grammatical representation for a language and provides an interpreter to deal with this grammar. This pattern is often used when a problem can be easily expressed in a language, but the solution to the problem is not immediately obvious. By using the interpreter design pattern, a developer can create a language that is specifically designed to solve a particular problem, and then provide an interpreter that can evaluate sentences written in that language. This allows users to write sentences in the language that the interpreter can understand and execute, which can be a useful way to solve complex problems.

To use the interpreter design pattern, a developer first defines a grammar for a language that can be used to solve a specific problem. This grammar defines the syntax and structure of the language, including the rules for how words and phrases can be combined to form valid sentences. Once the grammar has been defined, the developer can create an interpreter that can evaluate sentences written in the language. The interpreter uses the rules defined in the grammar to analyze each sentence and determine its meaning, and then executes the instructions that are specified in the sentence.

Is it complex, right? Let’s try this.

The interpreter design pattern is a way for a computer program to understand commands that are written in a specific language. Imagine you have a robot that can do different things, like move forward, turn left, and make a noise. The interpreter design pattern helps the robot understand what you want it to do when you tell it to do something using the specific language that it knows. For example, if you say “move forward” to the robot, it will understand that you want it to move in the direction it is facing. If you say “turn left,” the robot will understand that you want it to rotate its body to the left. This pattern is a useful way for programs to understand complex commands and execute them accordingly.

The first step in using the interpreter design pattern in C# is to identify the language that needs to be interpreted. This involves defining the grammar of the language, which includes the rules for constructing valid sentences in the language as well as the vocabulary that can be used. Once the language has been defined, the next step is to create a parser that can interpret sentences in the language and create an abstract syntax tree (AST) that represents the structure of the sentence. The AST can then be used to evaluate the sentence and produce the appropriate result.

The second step in using the interpreter design pattern in C# is to create the interpreter classes that will be used to evaluate the abstract syntax tree (AST) generated by the parser. These classes should implement the IExpression interface, which defines the method Evaluate() that is used to evaluate the AST. The Evaluate() method should take the AST as input and produce the appropriate result.

Here is an example of how you might implement the IExpression interface and use it to create an interpreter class in C#:

public interface IExpression
{
int Evaluate();
}

public class NumberExpression : IExpression
{
private int number;

public NumberExpression(int number)
{
this.number = number;
}

public int Evaluate()
{
return number;
}
}

In this example, the NumberExpression class represents an expression that evaluates to a specific number. The Evaluate() method simply returns the number that was provided when the NumberExpression instance was created. You can create other interpreter classes to represent other types of expressions in the language, such as arithmetic operations and function calls.

The third step in using the interpreter design pattern in C# is to use the parser and interpreter classes to evaluate sentences in the language. This typically involves the following steps:

1. Parse the sentence to be evaluated using the parser, which will generate an AST representing the structure of the sentence.

2. Traverse the AST and create an instance of the appropriate interpreter class for each node in the tree. For example, if the AST has a node representing an addition operation, you would create an AdditionExpression interpreter class to evaluate that node.

3. Evaluate the AST by calling the Evaluate() method on the root node of the tree. This will recursively evaluate each node in the tree, starting at the root and working down to the leaf nodes.

4. Return the result of the evaluation.

To continue the interpreter design pattern, you could create additional classes that implement the IExpression interface and represent other types of expressions that can be evaluated. For example, you might create a AdditionExpression class that can evaluate the result of adding two numbers, or a VariableExpression class that can evaluate the value of a variable in a given context. You could also create a Context class to store the values of variables and provide them to VariableExpression objects when they are evaluated.

Here is an example of how these classes might be implemented in C#:

public class AdditionExpression : IExpression
{
private IExpression left;
private IExpression right;

public AdditionExpression(IExpression left, IExpression right)
{
this.left = left;
this.right = right;
}

public int Evaluate()
{
return left.Evaluate() + right.Evaluate();
}
}

public class VariableExpression : IExpression
{
private string name;
private Context context;

public VariableExpression(string name, Context context)
{
this.name = name;
this.context = context;
}

public int Evaluate()
{
return context.Lookup(name);
}
}

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

public Context()
{
this.variables = new Dictionary<string, int>();
}

public void Assign(string name, int value)
{
variables[name] = value;
}

public int Lookup(string name)
{
return variables[name];
}
}

To use the interpreter design pattern shown in the code, you would first need to create instances of the classes that implement the IExpression interface. For example, you could create an AdditionExpression object by passing in two IExpression objects as arguments to its constructor. You could then evaluate the expression by calling the Evaluate() method on the AdditionExpression object.

IExpression left = new NumberExpression(2);
IExpression right = new NumberExpression(3);
IExpression additionExpression = new AdditionExpression(left, right);
int result = additionExpression.Evaluate();
// result will be 5

In the case of a VariableExpression, you would need to create a Context object that contains the variable values, and pass the Context object as an argument to the VariableExpression constructor.

var context = new Context();
context.Assign("x", 2);
context.Assign("y", 3);

IExpression x = new VariableExpression("x", context);
IExpression y = new VariableExpression("y", context);
IExpression additionExpression = new AdditionExpression(x, y);
int result = additionExpression.Evaluate();
// result will be 5

Since we wrote too many expressions, you may have thought of lambda expression. Yes, the interpreter design pattern shown in the code uses a similar approach to that of lambda expressions in that it uses objects that implement a common interface (in this case, the IExpression interface) to represent and evaluate expressions. However, there are some key differences between the two.

Unlike lambda expressions, which are a feature of many programming languages and can be used to create anonymous functions, the interpreter design pattern uses classes and objects to represent expressions. This means that the expressions must be defined using classes that implement the IExpression interface, and the objects representing the expressions must be created and used explicitly.

Additionally, the interpreter design pattern is a behavioral design pattern, whereas lambda expressions are a language feature. Behavioral design patterns focus on how objects interact and operate together, whereas lambda expressions are a way to define and use functions in a concise and flexible manner.

Overall, while the two approaches have some similarities, they are used for different purposes and have different strengths and limitations.

OK. Let’s continue the design pattern.

Imagine that you are working on a project for a company that specializes in manufacturing and distributing electronic components. The company receives orders from its customers in the form of purchase orders, which specify the type and quantity of components that the customer wants to purchase.

The purchase orders are written in a simple language that consists of the following elements:

  • Product codes: These are alphanumeric codes that identify the components that the customer wants to purchase.
  • Quantities: These are integers that specify the number of units of each component that the customer wants to purchase.
  • Operators: These are symbols that indicate how the quantities of different components should be combined.

For example, the following purchase order specifies that the customer wants to purchase 10 units of product A, 5 units of product B, and 15 units of product C:

“A10 B5 C15”

To process these purchase orders, you could use the interpreter design pattern to define a language grammar and then use it to interpret the sentences written in the language.

Here is some example code that demonstrates how you might do this in C#:

using System;
using System.Collections.Generic;

namespace InterpreterExample
{
// The 'AbstractExpression' class
abstract class Expression
{
public abstract void Interpret(Context context);
}

// The 'TerminalExpression' class
class ProductExpression : Expression
{
private string productCode;

public ProductExpression(string productCode)
{
this.productCode = productCode;
}

public override void Interpret(Context context)
{
context.AddProductToOrder(productCode);
}
}

// The 'NonterminalExpression' class
class NumberExpression : Expression
{
private int number;

public NumberExpression(int number)
{
this.number = number;
}

public override void Interpret(Context context)
{
context.SetQuantityForLastProduct(number);
}
}

// The 'Context' class
class Context
{
private Dictionary<string, int> order;

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

public void AddProductToOrder(string productCode)
{
order[productCode] = 0;
}

public void SetQuantityForLastProduct(int quantity)
{
string[] productCodes = new string[order.Keys.Count];
order.Keys.CopyTo(productCodes, 0);
string lastProductCode = productCodes[productCodes.Length - 1];
order[lastProductCode] = quantity;
}
}
}

The Context class is used to store the state of the interpreter as it processes the purchase order. It has a Dictionary called order that stores the products and quantities in the order.

The AddProductToOrder method adds a product to the order and sets its quantity to 0. The SetQuantityForLastProduct method sets the quantity of the last product in the order.

These methods are called by the Interpret methods of the ProductExpression and NumberExpression classes, respectively, as the interpreter processes the purchase order. They allow the interpreter to update the state of the Context object as it interprets the expressions in the order.

In other words, as the interpreter processes the purchase order, it calls the Interpret methods of the ProductExpression and NumberExpression classes to add products and set quantities in the order. These methods update the state of the Context object by adding products to the order dictionary and setting the quantities of the products.

This allows the interpreter to keep track of the state of the order as it processes the expressions in the purchase order. Once all of the expressions have been interpreted, the order is complete and can be printed or stored for further processing.

By the way, maybe you noticed that there is terms of TerminalExpression and NonTerminalExpression. In the context of the interpreter design pattern, a terminal expression is an expression that does not have any child expressions. It represents a leaf node in the expression tree, and it typically represents a simple value or an atomic operation that cannot be further decomposed.

For example, in a grammar for a simple mathematical expression language, a terminal expression might represent a number or a mathematical operator (such as +, -, *, or /). In a grammar for a command language, a terminal expression might represent a command verb or a command argument.

Terminal expressions are typically used to represent the basic building blocks of the language, and they are interpreted by the interpreter to perform the necessary actions or calculations. Nonterminal expressions, on the other hand, are used to represent more complex expressions that are composed of multiple terminal and nonterminal expressions, and they are interpreted by the interpreter to combine the actions or calculations of their child expressions.

For a result, you might be use these like the given below:

class Program
{
static void Main(string[] args)
{
// Parse the purchase order
var context = new Context();
var expressions = new List<Expression>();

var input = "A10 B25 C3";
string[] tokens = input.Split(' ');
foreach (string token in tokens)
{
if (char.IsLetter(token[0]))
{
expressions.Add(new ProductExpression(token[0].ToString()));
}
else
{
expressions.Add(new NumberExpression(int.Parse(token.Substring(1))));
}
}

// Interpret the purchase order
foreach (Expression expression in expressions)
{
expression.Interpret(context);
}

// Print the order
foreach (KeyValuePair<string, int> item in context.Order)
{
Console.WriteLine("Product: " + item.Key + ", Quantity: " + item.Value);
}
}
}

This code defines a grammar for the purchase order language that consists of product expressions and number expressions. The product expressions represent the product codes in the purchase order, and the number expressions represent the quantities of each product.

The code then parses the input string by splitting it into tokens and creating an expression for each token. If the token is a letter, it creates a product expression. If the token is a number, it creates a number expression.

Finally, the code interprets the expressions by calling the Interpret method on each one. This causes the Interpret method to be called on the product expressions, which adds the products to the order, and on the number expressions, which sets the quantities for the products in the order.

The resulting order is then printed to the console.

UML Diagram

Created on stackedit.io

In this diagram, the Context class holds data that is used in the interpretation process, and the AbstractExpression class defines an interface for interpreting the data. The TerminalExpression and NonterminalExpression classes are derived from AbstractExpression and provide specific implementations of the interpret method. The NumberExpression and VariableExpression classes are examples of TerminalExpression classes, and the AddExpression, SubtractExpression, MultiplyExpression, and DivideExpression classes are examples of NonterminalExpression classes.

Conclusion

The interpreter design pattern is a behavioral design pattern that allows you to define a grammar for a language and interpret sentences written in that language. It is useful when you need to parse and interpret a language that is simple, but not complex enough to warrant the use of a full-blown parser.

Some possible real-world uses of the interpreter design pattern include:

  • Parsing and interpreting simple mathematical expressions, such as “3 + 4 * 5”
  • Parsing and interpreting simple Boolean expressions, such as “A and (B or C)”
  • Parsing and interpreting simple command languages, such as “move north 5 steps” or “turn left 90 degrees”
  • Parsing and interpreting simple configuration files or scripts

In general, the interpreter design pattern can be useful whenever you need to parse and interpret a simple language and perform actions based on the structure and content of the sentences in that language.

Thanks for reading! If you found the article helpful, you can clap and follow. So you will notified of new articles.

# Reference

It was created with the help of ChatGPT AI.

--

--

Göksu Deniz

Software Engineer, passionate about creating efficient solutions. Skilled in mentoring teams to deliver successful projects. Always exploring new tech trends.