Interpreter

When you need to convert a data source from one form into another, you need to interpret it. It can be as simple as turning a binary data sequence into a list of text lines by looking for new-line characters.

As the complexity of the analysis function grows and the number of ways the analysis can be configured increases, the likelihood you have accidentally created a report processing or query language increases.

Translation of such a data stream can start with a Stream Builder, but when the transformation needs to change, you will need a different builder. One is fine, and two are bearable, but you need a better tactic when they grow to three or more.

Instead of putting the whole filtering and action sequence into one method of one class, introduce the Interpreter pattern by making each transform a separate object in a Composite.

The GoF book describes the Interpreter pattern as defining a language, representing problems in the language, and then resolving them by interpreting those forms. It even starts with an example using Backus-Naur form to describe the classes in the system.

The wording is complicated, and the language does not match our current way of thinking. It’s possibly more correct nowadays to say we represent intentions in the language rather than problems.

I want to avoid using terms like language or grammar as they confuse the pattern explanation since most software engineers don’t knowingly write DSLs any more1. Instead, I want to refer to objects converting, querying, or analysing other objects.

The first example in the GoF book uses a Composite, which we must traverse to evaluate and construct an answer to a query. We can think of this first type of Interpreter as using objects in a structure to define an algorithm. Rather than have a fixed algorithm, we write the elements of it, such as filtering or recognising elements in a stream of input. This is the type of Interpreter from the example chapter. The structure of objects interprets another object or stream of data. I like to call this form interpreter-as-state, as it interprets, but how it interprets is by way of the state of the composite. An interpreter built from objects like this was the type used in the example in Chapter 10.

The unique thing about interpreter-as-state is that the pattern uses structured objects to transform, query, process or interpret something else. The structured objects remain constant and unaffected during the evaluation. The context often remains unchanged, and the result is a new object.

The second example in the GoF book shows something different and perhaps easier to comprehend. The second Interpreter pattern is one of an Interpreter object making changes to a structure. The Interpreter object traverses the composite and produces a result. It’s used to rewrite a boolean expression structure. First, by replacing variables with constants, then replacing variables with expressions. In short, this version of the Interpreter is a pattern representation of updating immutable data structures. Or, to put it another way, and the way I prefer to refer to it, it’s a monad.

The important thing about the monad is that this pattern adjusts the structured objects based on the context and the Interpreter object which walks it. The structure is the thing that changed. The Interpreter either modifies the structure objects or returns a new structure using similar types. You could also think of this version of the pattern as a reinterpreter.

I am yet to discover examples where both change at the same time. Either the structure interprets the context or is interpreted in light of the context. This dichotomy seems relevant, so I distinguish between the two patterns as each appears to solve a different problem, even if the solutions look similar on the surface. It’s another pattern suffering from inappropriate aggregation due to the solution-oriented approach.

Both Interpreter styles effectively represent solutions to many more problems than initially claimed. The examples show how to transform compounds of any form and build up small but effective DSLs using objects.

So, instead of thinking about Interpreter as defining a grammar, consider it to be a way to write procedures, queries, or transforms dynamically or how to mutate structured data. It does not need to be a tree to be a useful pattern. It does not need to define a language in a traditional sense. It does not need to interpret anything directly but only consume something and generate some interpretation.

A general misconception, and the third interpretation of the pattern, is that the interpreter pattern is about writing a parser—even though the GoF book explicitly states it is not. The book doesn’t explain how to create the tree in the first place, but a monad Interpreter could be a useful pattern in matching and parsing a small language into a structure that could be an interpreter-as-state Interpreter.

“How do you want me to transform this for you?”

1

Since the advent of object-oriented design and programming, the number of explicit DSLs I’ve seen has reduced dramatically. But, because the interaction of objects with structure against streams of structured and unstructured data is, in effect, a DSL, the number of actual DSLs has increased. This gap in understanding has possibly held back the interpreter pattern from more mainstream use.