The interpreter pattern

The interpreter pattern is not really used, but it can be really useful. Usually, this pattern is described in terms of formal grammar but the area where it can be applied, can be extended.

You can refer to the following for more information: Lecture de chiffre Romain (http://www.oodesign.com/interpreter-pattern.html) and https://en.wikipedia.org/wiki/Roman_numerals.

Roles

The interpreter pattern defines an object representation of a language grammar in order to evaluate some expression written in this language by interpreting them.

This pattern can be used to interpret some expressions that are represented as a hierarchical tree. It may be applied when:

  • Grammar of expression is simple
  • Evaluation doesn't need to be quick

Some examples where this pattern can be used are:

  • In rule engines
  • To add some functionality to the composite pattern

Design

The implementation of this pattern is seen in the use of the composite pattern applied to represent a grammar (refer to the The composite pattern in Chapter 3, Structural Patterns – Composite and Flyweight). The difference is that the interpreter pattern defines the behavior while the composite defines only the structure.

The UML class diagram of the pattern is as follows:

Design

Participants

Participants to this pattern are as follows:

  • AbstractExpression: This defines the interpret() method that is common to all the nodes in the abstract syntax tree.
  • TerminalExpression: This implements the interpret method associated with terminal symbols of the grammar. Each terminal symbol of the grammar requires a concrete class.
  • NonTerminalExpression: This implements the interpret method and can also contain other AbstractExpression instances.
  • Context: This contains information that is global to the interpreter. For example, the actual values of variables.
  • Client: This builds an abstract syntax tree that is assembled from instances of NonTerminalExpression and TerminalExpression. It will be our demo usage of the pattern.

Collaboration

The client builds the abstract syntax tree, initializes the context of the interpreter, and then invokes the interpret method.

The interpret method at the TerminalExpression and NonTerminalExpression node uses the context to store and access the state of the interpreter.

Illustration

We want to create a roman number converter; you know the ones that interpret that XIV means 14 in decimals. The main purpose of our example is to write a roman number and our converter will tell us the decimal value.

Implementation

Open the InterpreterPattern.xcodeproj file and see how we have implemented the pattern.

Note

For this pattern, all the code has been added to the main.swift class so that it can be easily exported into Playground if you want to see the live execution of the code.

Before starting to understand the code, have a look at the following link to understand how roman numbers work and how they are written.

You can see some rules about the roman numerals and also a roman numeral chart at http://4thgradecrocs.weebly.com/roman-numerals.html and how the roman system works can be seen at https://en.wikipedia.org/wiki/Roman_numerals.

For the pattern, we will use one string extension that lets us do a substring easily just by passing the number of characters we want to ignore in the argument:

extension String {
  func subStringFrom(pos: Int) -> String {
    var substr = ""
    let start = self.startIndex.advancedBy(pos)
    let end = self.endIndex
    let range = start..<end
    substr = self[range]
    return substr
  }
}

The expression to be interpreted is a string that is put in the context:

class Context {
  var input: String!
  var output: Int = 0
  
  init(input: String){
    self.input = input
  }
}

This class will help us to work while we apply the pattern; it consists of the remaining unparsed roman numeral strings and also the result of the numerals that are already parsed.

The context is passed to one of four sub-interpreters based on the type of interpreting (thousand, hundred, ten, and one). In this example, only TerminalExpressions are used.

Next, we will define our AbstractExpression class. This class must implement the interpret() method and define the methods that will be overridden in the subclasses:

class Expression {
  func interpret(context: Context) {
    if context.input.characters.count == 0 {
      return
    }
    
    if context.input.hasPrefix(nine()){
      context.output = context.output + (9 * multiplier())
      context.input = context.input.subStringFrom(2)
    } else  if context.input.hasPrefix(four()){
      context.output = context.output + (4 * multiplier())
      context.input = context.input.subStringFrom(2)
    } else  if context.input.hasPrefix(five()){
      context.output = context.output + (5 * multiplier())
      context.input = context.input.subStringFrom(1)
    }
    
    while context.input.hasPrefix(one()) {
      context.output = context.output + (1 * multiplier())
      context.input = context.input.subStringFrom(1)
    }
  }
  
  func one() -> String {
    fatalError("this method must be implemented in a subclass")
  }
  
  func four() -> String {
      fatalError("this method must be implemented in a subclass")
  }
  
  func five() -> String {
      fatalError("this method must be implemented in a subclass")
  }
  func nine() -> String {
      fatalError("this method must be implemented in a subclass")
  }
  func multiplier() -> Int {
      fatalError("this method must be implemented in a subclass")
  }
}

The Expression class consists of the interpret method, which receives the context. Based on the current object, it uses specific values for thousand, hundred, ten, one, and also a specific multiplier.

The one(), four(), five(), nine(), and multiplier() methods of the Expression class are abstract. They will be implemented in our Concrete TerminalExpressions class

We can now implement our four TerminalExpression classes. Each of them override the one(), four(), five(), nine() and the multiplier() methods. These methods will then be interpreted depending on if we are in a thousand, hundred, ten or one expressions. Indeed, these classes are used to define each specific expression. Usually, these classes implement the interpret method, but here it is already defined in the base expression class and each of the TerminalExpression class defines its behavior by implementing the abstract methods: one(), four(), five(), nine(), and multiplier(). This is a template method (refer to the The template method section in Chapter 5, Behavioral Patterns – Strategy, State, and Template Method):

class ThousandExpression: Expression {
  override func one() -> String {
    return "M"
  }
  override func four() -> String {
    return " "
  }
  override func five() -> String {
    return " "
  }
  override func nine() -> String {
    return " "
  }
  override func multiplier() -> Int {
    return 1000
  }
}

class HundredExpression: Expression {
  override func one() -> String {
    return "C"
  }
  override func four() -> String {
    return "CD"
  }
  override func five() -> String {
    return "D"
  }
  override func nine() -> String {
    return "CM"
  }
  override func multiplier() -> Int {
    return 100
  }
}

class TenExpression: Expression {
  override func one() -> String {
    return "X"
  }
  override func four() -> String {
    return "XL"
  }
  override func five() -> String {
    return "L"
  }
  override func nine() -> String {
    return "XC"
  }
  override func multiplier() -> Int {
    return 10
  }
}

class OneExpression: Expression {
  override func one() -> String {
    return "I"
  }
  override func four() -> String {
    return "IV"
  }
  override func five() -> String {
    return "V"
  }
  override func nine() -> String {
    return "IX"
  }
  override func multiplier() -> Int {
    return 1
  }
}

The pattern is now written; we only have to test it. Before writing our test code, we will create a helper RomanToDecimalConverter() class having a calculate() method that returns the result of the conversion:

class RomanToDecimalConverter {
  var tree = [ThousandExpression(), HundredExpression(), TenExpression(),OneExpression()]

  func calculate(romanString: String) -> Int {
    let context = Context(input: romanString)
    for t in tree {
      t.interpret(context)
    }
    return context.output
  }
}

This class is responsible to build the syntax tree representing our specific value; the roman number, in the language defined by the grammar.

Note that we can only add our terminal expressions to the array in a specific order: from thousand to one expression, as we will parse the roman string from left to right.

We call the interpret method after the syntax tree is build. We return the context.output value once all expressions of the pattern are executed, which corresponds to the decimal result.

We will add a new method to our RomanToDecimalConverter before writing our test code. This will validate that the roman number that we are trying to convert is correct: otherwise, a message will be displayed informing us that our number is not a roman number. The added code is highlighted in the following:

enum FormatError: ErrorType {
  case RomanNumberFormatError
}

//Helper
class RomanToDecimalConverter {
  static let pattern = "^M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})$"
  let validation = NSPredicate(format: "SELF MATCHES %@", pattern)
  
  var tree = [ThousandExpression(), HundredExpression(), TenExpression(),OneExpression()]
  
  func calculate(romanString: String) throws -> Int {
    guard validate(romanString) else {
      throw FormatError.RomanNumberFormatError
    }
    
    let context = Context(input: romanString)
    for t in tree {
      t.interpret(context)
    }
    return context.output
  }
  
  func validate(romanString: String) -> Bool {
    return validation.evaluateWithObject(romanString)
  }
}

We first define a new enum function that we call FormatError of type ErrorType. We will use this to throw an exception when the format of the roman number is incorrect.

Then we have a static string pattern in the RomanToDecimalConverter class that contains our regular expression and also an instance constant called validation that will handle the validation process through an NSPredicate object. The purpose of this object is to define the logical conditions needed to constrain a search either for a fetch or for an in-memory filtering.

In the calculate method, we had a guard statement that executed the validate function, if the validate function returns false (meaning the number is incorrect), a FormatError.RomanNumberFormatError exception is raised; otherwise, we will continue the operation and calculate the decimal value using the pattern. Note the addition of the throws keyword just before the return type of the method. It means that this method can throw errors.

The validate() method calls the evaluateWithObject method of the NSPredicate instance by passing the roman number in an argument. This method returns true if the format is correct, otherwise it will return false.

Note

The regex expression used in this code comes from the following URL and is very well explained at: http://stackoverflow.com/questions/267399/how-do-you-match-only-valid-roman-numerals-with-a-regular-expression

For more information about NSPredicate, you can refer to this site: https://realm.io/news/nspredicate-cheatsheet/

Now we can write our test code like the following:

let romanNumberToTest = ["XIV", "MCCMXXVIII","MCMXXVIII"]
var converter = RomanToDecimalConverter()
for roman in romanNumberToTest {
  var decimal = try? converter.calculate(roman)
  guard (decimal != nil) else {
    print("(roman) is not a correct roman number")
    continue
  }
  print(decimal!)
}

We initialize an array with some roman numbers and then initialize our RomanToDecimalConverter converter object and loop over all elements to make the calculations.

We are using the following statement:

var decimal = try? converter.calculate(roman)

The decimal will return nil if an exception is raised. So, the next three lines help us to print an appropriate message when a roman number doesn't have the correct format.

With the three values of our array and after building and running the project, you'll obtain the following result:

Implementation

The preceding result given by the program inform us that MCCMXXVIII has a wrong roman number format and the two others numbers are well converted with the 14 decimal value for the first roman number and 1928 for the second roman number.

You can try running the code using the roman numeral chart given at the beginning of this section.

You can see that this pattern is really interesting. You have already seen that the composite and template method patterns have also been used to build and implement our interpreter pattern with our use case.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset