Interpreter Pattern in Ruby: Building Domain-Specific Languages

Explore the Interpreter Pattern in Ruby, a powerful design pattern for creating domain-specific languages. Learn how to implement this pattern using Ruby's metaprogramming capabilities, and understand its benefits and applications.

6.3 Interpreter Pattern

Introduction

The Interpreter pattern is a behavioral design pattern that provides a way to evaluate language grammar or expressions. It is particularly useful for designing and implementing simple domain-specific languages (DSLs) and is often employed in scenarios where you need to interpret or parse custom languages. In Ruby, the Interpreter pattern can be implemented effectively by leveraging its dynamic and metaprogramming capabilities.

Intent

The primary intent of the Interpreter pattern is to define a representation for a language’s grammar and provide an interpreter to process sentences in that language. This pattern is particularly useful when the language to be interpreted is simple and can be represented by a grammar.

Problem Addressed

The Interpreter pattern addresses the problem of parsing and interpreting custom languages. It is ideal for situations where you need to evaluate expressions or commands written in a specific language. This pattern allows you to build a system that can understand and execute instructions defined in a custom language, making it easier to extend and maintain.

Key Participants

  1. AbstractExpression: Declares an abstract interpret method that is implemented by all concrete expressions.
  2. TerminalExpression: Implements the interpret method for terminal symbols in the grammar.
  3. NonTerminalExpression: Implements the interpret method for non-terminal symbols, which are composed of other expressions.
  4. Context: Contains information that is global to the interpreter, such as variable values.
  5. Client: Builds the abstract syntax tree (AST) representing the language’s grammar and invokes the interpret method.

Applicability

Use the Interpreter pattern when:

  • You have a simple language to interpret.
  • The grammar is stable and not expected to change frequently.
  • You want to extend the language easily by adding new expressions.
  • You need to evaluate expressions or commands dynamically.

Implementing the Interpreter Pattern in Ruby

Let’s explore how to implement the Interpreter pattern in Ruby by building a simple arithmetic interpreter. This interpreter will evaluate expressions consisting of numbers and basic arithmetic operations like addition and subtraction.

Step 1: Define the Abstract Expression

First, we define an abstract class Expression with an interpret method that all concrete expressions will implement.

1# Abstract Expression
2class Expression
3  def interpret(context)
4    raise NotImplementedError, "#{self.class} has not implemented method '#{__method__}'"
5  end
6end

Step 2: Implement Terminal Expressions

Terminal expressions represent the simplest elements of the language, such as numbers in our arithmetic interpreter.

 1# Terminal Expression for Numbers
 2class Number < Expression
 3  def initialize(value)
 4    @value = value
 5  end
 6
 7  def interpret(context)
 8    @value
 9  end
10end

Step 3: Implement Non-Terminal Expressions

Non-terminal expressions represent operations or rules that are composed of other expressions. For our arithmetic interpreter, we will implement addition and subtraction.

 1# Non-Terminal Expression for Addition
 2class Add < Expression
 3  def initialize(left, right)
 4    @left = left
 5    @right = right
 6  end
 7
 8  def interpret(context)
 9    @left.interpret(context) + @right.interpret(context)
10  end
11end
12
13# Non-Terminal Expression for Subtraction
14class Subtract < Expression
15  def initialize(left, right)
16    @left = left
17    @right = right
18  end
19
20  def interpret(context)
21    @left.interpret(context) - @right.interpret(context)
22  end
23end

Step 4: Define the Context

The context holds any global information needed during interpretation. In our simple example, we don’t require a complex context, but it can be extended to include variables or other state information.

1# Context class (can be extended as needed)
2class Context
3  # Add context-specific methods and data here
4end

Step 5: Build the Abstract Syntax Tree (AST)

The client constructs the AST using the defined expressions. For example, to evaluate the expression (5 + 3) - 2, we build the AST as follows:

 1# Building the AST for the expression (5 + 3) - 2
 2context = Context.new
 3expression = Subtract.new(
 4  Add.new(Number.new(5), Number.new(3)),
 5  Number.new(2)
 6)
 7
 8# Evaluate the expression
 9result = expression.interpret(context)
10puts "Result: #{result}" # Output: Result: 6

Roles of Context and Abstract Syntax Trees

  • Context: Provides the necessary environment for interpretation, such as variable values or global state.
  • Abstract Syntax Tree (AST): Represents the hierarchical structure of the language’s grammar. Each node in the AST corresponds to an expression in the language.

Benefits of the Interpreter Pattern

  1. Ease of Language Extension: Adding new expressions or operations is straightforward, making it easy to extend the language.
  2. Flexibility: The pattern allows for dynamic interpretation of expressions, enabling runtime evaluation.
  3. Separation of Concerns: The pattern separates the grammar representation from the interpretation logic, promoting clean and maintainable code.

Ruby’s Unique Features

Ruby’s metaprogramming capabilities make it particularly well-suited for implementing the Interpreter pattern. You can dynamically define methods, manipulate classes, and create domain-specific languages with ease.

Differences and Similarities

The Interpreter pattern is often confused with the Strategy pattern. While both involve defining a family of algorithms, the Interpreter pattern focuses on evaluating expressions based on a grammar, whereas the Strategy pattern is about selecting an algorithm at runtime.

Try It Yourself

Experiment with the interpreter by extending it to support multiplication and division. Implement the Multiply and Divide classes and modify the AST to evaluate expressions like (5 * 3) / 2.

Visualizing the Interpreter Pattern

Let’s visualize the structure of the Interpreter pattern using a class diagram.

    classDiagram
	    class Expression {
	        +interpret(Context): int
	    }
	    class Number {
	        +interpret(Context): int
	    }
	    class Add {
	        +interpret(Context): int
	    }
	    class Subtract {
	        +interpret(Context): int
	    }
	    class Context {
	    }
	
	    Expression <|-- Number
	    Expression <|-- Add
	    Expression <|-- Subtract

Knowledge Check

  • What is the primary intent of the Interpreter pattern?
  • How does the Interpreter pattern differ from the Strategy pattern?
  • What role does the context play in the Interpreter pattern?

Summary

The Interpreter pattern is a powerful tool for building domain-specific languages and interpreting custom languages. By leveraging Ruby’s metaprogramming capabilities, you can create flexible and extensible interpreters that are easy to maintain and extend. Remember, the key to mastering the Interpreter pattern is understanding the grammar of the language you wish to interpret and constructing a corresponding abstract syntax tree.

Quiz: Interpreter Pattern

Loading quiz…

Remember, this is just the beginning. As you progress, you’ll build more complex and interactive interpreters. Keep experimenting, stay curious, and enjoy the journey!


Revised on Thursday, April 23, 2026