A great developer is valuable. We all know it.
But it takes more than just talent to achieve a high-quality outcome.
There are dozens of well-documented techniques and practices that can help ensure that your code is maintainable, readable, and resilient. Especially when you have an application that needs to interpret strings into another type of expression. Then you can use the Interpreter design pattern.
The Interpreter design pattern is a behavioral software design pattern used to define a grammar for specifying a language and interpreting sentences in that language. It is helpful for specialized or highly technical languages, such as scripting languages or mathematical notation.
In this post, you will learn how the Interpreter pattern works in C# and in what cases you can use it to simplify your problems, along with examples to help you understand how it works.
What is the Interpreter design pattern?
In 1994, four programmers, Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, decided to introduce design patterns consisting of reusable solutions to commonly occurring problems. The purpose of these patterns was to document successful designs and architectures used by programmers multiple times to solve real-world problems.
They divided 23 design patterns into three basic categories:
The key focus of this blog post, interpreter design pattern, falls in the last category, i.e., behavioral design patterns.
The Interpreter design pattern is a behavioral design pattern that deals with communication and relations. In its essence, an interpreter design pattern creates a representation for the grammar of a simple language along with an interpreter to decode the grammatical representation.
In this pattern, you would find an expression that works as an interface to interpret the context (which is global). This pattern is convenient in creating a menu of applications and parsing queries and translators.
When to use the Interpreter design pattern?
The interpreter design pattern has the most benefits when:
There is no complex grammar involved
An interpreter design pattern creates abstract syntax trees for the expressions you create. However, for complex grammar, these trees can take extensive space and time, leading to an unimaginable class hierarchy for grammar.
Parsing generators generally work faster than an interpreter design pattern in such cases. Therefore, we should use this pattern when the grammar is simple enough to remain manageable and can be converted to interpretable sentences.
When efficiency is not the focus
Interpreter design patterns can quickly parse through a language to deliver the solution. However, they are not as efficient as the state machine representation of the expressions.
Most efficient translators first convert the regular expressions into another form before translating. So, use them when your focus is reusability and easy, not efficiency.
When expressions can be represented in Syntax Trees
An interpreter design pattern needs a language you can read and decode. However, the expressions in the language must also be representable in the form of abstract syntax trees.
Who are the participants of the Interpreter pattern? C# Structural code
The UML structure of an interpreter design pattern consists of the following participants:
- AbstractExpression: This declares an interface common to all nodes (terminal and non-terminal). Each subclass must implement the Interpret () operation.
- TerminalExpression: This class must implement Interpret() operation defined by the abstract expression for terminal expressions. It requires an instance for every terminal symbol encountered in a sentence; for instance, every number in the expression “3+2+1” requires a terminal expression.
- NonterminalExpression: These implement non-terminal symbols in grammar, and a separate class must be defined for each grammatical rule R. For instance, in a postfix equation, this would include AdditionExpression, MultiplicationExpression, etc. The Interpret () operation generally calls itself recursively on variable instances this expression maintains.
- Context: Context contains global information and is the target of the interpretation. It is the input given to the interpreter for parsing and interpreting and the output obtained from it.
- Client: This class in C# builds the abstract syntax tree provided the language is available to it. The syntax tree represents any particular sentence of that language and consists of both terminal and non-terminal Expressions.
Now let’s look at a sample C# code representing the above UML structure:
public class Context
{
public string StringToParse { get; set; }
public Context(string stringToParse)
{
StringToParse = stringToParse;
}
}
public interface IExpression
{
void Interpret(Context context);
}
public class TerminalExpression : IExpression
{
public void Interpret(Context context)
{
Console.WriteLine("Terminal Expression " +
"Output in {0}.", context.StringToParse);
}
}
public class NonterminalExpression : IExpression
{
public IExpression Expression1 { get; set; }
public IExpression Expression2 { get; set; }
public void Interpret(Context context)
{
Console.WriteLine("Nonterminal Expression " +
"Output in {0}.", context.StringToParse);
Expression1.Interpret(context);
Expression2.Interpret(context);
}
}
And the usage:
Context context = new Context("Method Poet");
NonterminalExpression output = new NonterminalExpression();
output.Expression1 = new TerminalExpression();
output.Expression2 = new TerminalExpression();
output.Interpret(context);
The output for the above code will be:
Nonterminal Expression Output in Method Poet.
Terminal Expression Output in Method Poet.
Terminal Expression Output in Method Poet.
This code snippet follows the implementation method of the Interpreter Design Pattern. It creates a class NonterminalExpression
that implements the IExpression
interface, which takes a single method Interpet
. The instance of the NonterminalExpression
class has two properties, Expression1
and Expression2
, of type IExpression
.
As per the rules, each Interpret method takes the context as the argument as it is the target of interpretation in this design method and carries global information used throughout different classes.
Finally, the client class acts as the driver and uses the code above to produce the output.
The code snippet here is written precisely in the form of a UML design to give a clear picture of the Interpreter design pattern. However, in most real-world cases, you will find three layers of code for this pattern, including a context, an expression, and an interpreter.
In the next section, let’s look at the real-world usage of the Interpreter design pattern.
Interpreter pattern – Real-world example in C#
The interpreter pattern is not extensively used. However, when its need arises, it serves the purpose. Besides compilers and programming language interpreters, Google translate is a popular example of an interpreter design pattern. Similarly, the mathematical equations you enter into your computer, for instance, “(16/2)*4,” gives you 32. This is another use case of the Interpreter pattern that converts integral expressions from one form to another.
By understanding the Interpreter pattern, we know it can work as a translator. So, in this C# example, we will be converting numbers into words in English, which has been used in online ordering systems, school logs, and other places. Furthermore, you can extend this implementation to represent sentences and numbers in English, and vice versa, which can be helpful in retail and business.
public class NumberContext
{
public int Number { get; set; }
public string Result { get; set; } = string.Empty;
public NumberContext(int number)
{
Number = number;
}
}
public interface INumberExpression
{
public void Interpret(NumberContext context);
}
public class NumberExpresion : INumberExpression
{
public void Interpret(NumberContext context)
{
var stringNumber = context.Number.ToString();
var numberTranslations = new string[]
{"Zero",
"One",
"Two",
"Three",
"Four",
"Five",
"Six",
"Seven",
"Eight",
"Nine"
};
foreach (var character in stringNumber)
{
context.Result += $"{numberTranslations[int.Parse(character.ToString())]}-";
}
context.Result = context.Result.Remove(context.Result.Length - 1);
}
}
And the usage.
var expression = new NumberExpresion();
var output = new NumberContext(3456);
expression.Interpret(output);
string result = output.Result;
Console.WriteLine("The string is:");
Console.WriteLine(result);
The output is:
The string is:
Three-Four-Five-Six
This example converts integers from 1 to 9 into words. The context consists of information about the integer input as well as the string output. The IExpression acts as an interface to the class Expression. This class carries out the main implementation and rules of the program. The Interpreter uses these rules in Expression to produce the result.
The program produces “Three” against the input 3. Similarly, it would produce “two” against input 2, “one” against input 1, and so on.
What are the advantages and disadvantages of using the Interpreter pattern?
Interpreter Design Pattern has many pros and cons. Let’s look at them:
Pros:
- Extendable and modifiable: With the Interpreter pattern, extending or making changes to the grammar becomes highly convenient. Since it uses classes to represent each grammar rule, every child class automatically inherits the properties and methods of the parent class. In this way, you can add changes and extensions to the child classes to modify, extend and change grammatical rules incrementally.
- Easy Implementation: This pattern represents grammar classes as syntax trees, where each node represents a grammar rule. Each class has a similar implementation, so once you create one class, a few changes can lead to other classes. Hence, the implementation of this pattern is straightforward.
- Interpreting Expressions Differently: Expression classes define how the expression will be evaluated. You can add new operations in that class to add more functionality to the interpretation, for instance, printing expressions with proper indentations.
Cons:
- Complex grammar rules are hard to implement: With an Interpreter pattern, you need a separate class for each grammatical rule. Consider, for example, this expression, 3+1. The Interpreter pattern will define two expression classes, NumberClass and PlusClass. Now, imagine if the expression involves every symbol ever involved in mathematics, which according to some sources, reaches a value of 10,000+. Using the Interpreter Pattern for this case would mean defining 10,000 different classes to represent each rule. Hence, for a large number of rules, using this pattern becomes an ordeal in itself.
Patterns related to Interpreter design pattern
The interpreter pattern is related to the following GoF patterns:
- Composite Pattern – The abstract syntax tree, whose nodes are the classes in the Interpreter pattern, is an instance of a composite pattern. Every composite pattern contains an interpreter pattern. However, we define it as the interpreter pattern when dealing with a language.
- Flyweight Pattern – It deals with the position of terminal nodes in an abstract syntax tree.
- Visitor Pattern – It defines new operations within a single class and has implementation inside the Interpreter pattern.
- Iterator Pattern – Works like a cursor and allows traversal of the structure in the Interpreter pattern.
Important considerations when implementing Interpreter pattern
The Interpreter Pattern is merely an abstract definition of the problem solution. Implementing it is not as simple as understanding its hierarchy layout. Here are the following considerations you need to make while implementing this pattern as your solution:
- It carries no information about the implementation of the abstract syntax tree. You can use table-driven, recursive descent or the client for parsing.
- You don’t need to define Interpret in Expression Classes. If you need a new interpreter every time, the Visitor pattern object is a better choice as it will prevent repetitive definitions in every grammar class. Learn more about the Visitor pattern in a separate article about it.
- Use Flyweight pattern for sharing of terminal symbols. Grammar, for instance, those in computers, can have terminal symbols appearing in multiple places. Here sharing a single copy of those symbols with the Flyweight pattern is more economical and performance-friendly.
Conclusion
Design patterns are the perfect programming shortcuts.
The Interpreter pattern is no different. While you might find an interpreter pattern in other solutions, you can only categorize it as such if it involves any language interpretation. In the programming world, you will find it implemented in many compilers.
However, anywhere you encounter the problem of evaluating the grammar of a particular language or expressions of mathematical form, you can use the Interpreter pattern to ease your process.
Problems involving Interpreter patterns are less frequent than the other design patterns. However, the Interpreter pattern is your biggest asset when such a problem occurs and if it requires simplicity over efficiency.
This post explained the interpreter pattern along with a sample code, implementation considerations, and relation with other GoF patterns.
If you want to learn more about design patterns, check the separate in-depth article about design patterns in C#.