Design Patterns in Go: Interpreter, making sense of the world.

Introduction

The Interpreter pattern can be used to interpret and evaluate sentences in a language. The idea is to make a class for each symbol, both terminal and non-terminal and construct a syntax tree which can be evaluated and interpreted.

The interpreter pattern I will use here is probably one of the simplest implementations of this pattern. Before you use this pattern, what you need is a well-defined grammar for your language so that it can be evaluated and interpreted.

The simplest form of the interpreter looks like this:

This is not the whole picture, as we also need to build a syntax tree, which can be done in a parser, which is outside the scope of this article.

I think the idea will become much clearer when we implement this pattern

Implementation in Go

In an empty directory open your commandline or terminal and type:

go mod init github.com/interpreter

Now add a main.go file and add the following preliminaries:

package main

import (
	"fmt"
	"strings"
)

The Expression interface

The Expression interface looks like this:

type Expression interface {
	Interpret() bool
}

It has just the one method, Interpret() which returns a bool.

The Non-terminal expressions

There are two non-terminal expressions, the AndExpression and the OrExpression.

type AndExpression struct {
	expr1 Expression
	expr2 Expression
}

func (a *AndExpression) Interpret() bool {
	return a.expr1.Interpret() && a.expr2.Interpret()
}

type OrExpression struct {
	expr1 Expression
	expr2 Expression
}

func (o *OrExpression) Interpret() bool {
	return o.expr1.Interpret() || o.expr2.Interpret()
}

Again, quite straightforward I think. Both structs have two fields which implement the Expression interface.

In the AndExpression, the Interpret() method performs an and-operation as denoted by the ‘&&’, in the OrExpression the Interpret() method performs an or-operation, as denoted by the ‘||’.

The TerminalExpression struct

The TerminalExpression looks like this:

type TerminalExpression struct {
	data string
}

func (t *TerminalExpression) Interpret() bool {
	return strings.Contains(t.data, "hello")
}

All the TerminalExpression gets, is a piece of data of type string. The Interpret() method just looks if the data contains the word ‘hello’, and returns true if it does.

Testing it

Now let us see what we can do with it:

func main() {

	expression1 := &TerminalExpression{"hello world"}
	expression2 := &TerminalExpression{"goodbye world"}

	expression3 := &OrExpression{expression1, expression2}
	fmt.Println(expression3.Interpret())

	expression4 := &TerminalExpression{"hello everyone"}
	expression5 := &AndExpression{expression1, expression4}
	fmt.Println(expression5.Interpret())
}

Line by line:

  1. We construct two objects of type TerminalExpression, with different pieces of data
  2. Then we feed those into an OrExpression and interpret the OrExpression. Since one of the TerminalExpression objects contains the word ‘hello’, the Interpret() method will return true.
  3. Next we construct a new object of type TerminalExpression, also with ‘hello’ in its data
  4. We feed both the first and the new TerminalExpression-objects to the AndExpression. Since both objects contain ‘hello’, the Interpret() method should also return true.

Conclusion

Implementing this was quite easy. The hardest part will probably be building a parser so that these, and more expressions, can be put into some sort of Abstract Syntax Tree, and be evaluated from there.

The beauty of this thing is the fact that it forms a recursive datastructure, which translates quite elegantly into Go.

Leave a Reply

Your email address will not be published. Required fields are marked *