⬆️ ⬇️

DesignPatterns, Interpreter Template

Language compilers and interpreters are usually written in other languages ​​(at least it was first). For example, PhP was written in C. As you guessed in PhP, you can define, design and write your own compiler of your language. Of course, the language that we create will work slowly and will be limited, however it can be very useful.



And so create a problem that needs to be solved.


As a rule, when developing an application, we provide the user with some functionality (api), while developing an interface, we have to find a compromise between functionality and ease of use and it’s natural that the more functional the more confusing the interface is. Probably, you can simplify the interface without “cutting off” the application functionality. We offer our users a domain-oriented programming language (commonly called DSL, or Domain Specific Language), you can really extend the functionality of the application.

Of course, you can allow the user to create their own scripts in our system without an intermediate programming language:



$input = $_REQUEST['input']; // «print file_get_contents('/etc/passwd'); eval($input); 


')

It is obvious that this method can not be called normal. But if it is not obvious to you, then there are two reasons for this: security and complexity.



But our mini, intermediate language can help solve such problems, make it flexible, reduce the user's capabilities so that he does not break our system and is ready.

Suppose you need an application to develop a small testing system. Developers come up with questions and set the rules for evaluating answers. There are requirements that the answers to the questions should be evaluated without human intervention, although users may enter some answers in text fields.



Sample question:

How many planets in our solar system?

We can accept the answers "eight", "8" as the correct answers. Of course, this could be solved with the help of your favorite regular expressions, ^ 8 | eight $.

But unfortunately, not all developers know them. So let's implement a more user-friendly interface:



$input equals «8» or $input equals «»



You want to create a language that supports variables, the equals operator, and Boolean logic (or, and). Since we all love to call everything let's call our language, GoLogic.

Our language should be clearly expanded, since we foresee many requests for more complex functions. Let us leave aside the question of analyzing the input data and move on to the architecture of the mechanism for connecting these elements to generate a response.



Implementation


Even in our small language, there is a need to keep track of the many elements that are variables and string literals, and boolean AND and boolean OR and equality checks.

We divide them into classes:

image



Draw a small diagram:

image



A bit of code:

 abstract class Expression { private static $keycount = 0; private $key; abstract function interpret(InterpreterContext $context); function getKey() { if (!isset($this->key)) { self::$keycount++; $this->key = self::$keycount; } return $this->key; } } class LitralExpr extends Expression { private $value; function __construct($value) { $this->value = $value; } public function interpret(InterpreterContext $context) { $context->replace($this, $this->value); } } class InterpreterContext { private $exprstore = array(); function replace(Expression $exp, $value) { $this->exprstore[$exp->getKey()] = $value; } function lookup(Expression $exp) { return $this->exprstore[$exp->getKey()]; } } 




InterpreterContext provides only the interface for the $ exprstore associative array that we use to store data. The replace () method is passed a key and a value, and the lookup () method is also implemented to retrieve data.

In the Expression class, the abstract interpre () method is defined and a specific getKey () method; operating with a static counter value, it is returned as an expression descriptor; this is the method we use to index the data.

In the LiteralExpr class, a constructor is defined to which the value argument is passed. The interept () method needs to pass an object of type InterpreterContex in it simply call the replace () method

Now we define the remaining class VariableExpr

 class VariableExpr extends Expression { private $name; private $val; public function __construct($name, $val = null) { $this->name = $name; $this->val = $val; } public function interpret(InterpreterContext $context) { if (!is_null($this->val)) { $context->replace($this, $this->val); $this->val = null; } } function setValue($val) { $this->val = $val; } function getKey() { return $this->name; } } 




The constructor is passed two arguments (name and value), which are stored in the properties of the object. The class implements the setValue () method so that the client code can change the value at any time.

The interept () method checks whether the $ val property has a zero value, if there is a value, then its value will be stored in the InterpreterContext object. Then we set the $ val variable to null so that the repeated method call would not spoil the value of the variable with the same name stored in the InterpreterContext object by another instance of the VariableExpr object.

Now add some logic.



 abstract class OperatorExpr extends Expression { protected $l_op; protected $r_op; function __construct(Expression $l_op, Expression $r_op) { $this->l_op = $l_op; $this->r_op = $r_op; } function interpret(InterpreterContext $context) { $this->l_op->interpret($context); $this->r_op->interpret($context); $result_l = $context->lookup($this->l_op); $result_r = $context->lookup($this->r_op); $this->doInterpret($context, $result_l, $result_r); } protected abstract function doInterpret(InterpreterContext $context, $result_l, $result_r); } 




The doInterpret () method is an instance of the Template Method pattern. In this template, in the parent class, an abstract method is defined and called, the implementation of which is left to the child classes. This can simplify the development of specific classes, since the shared functions are controlled by the super class, leaving the child to concentrate on clear and understandable goals.



 class EqualsExpr extends OperatorExpr { protected function doInterpret(InterpreterContext $context, $result_l, $result_r) { $context->replace($this, $result_l == $result_r); } } class BoolOrExpr extends OperatorExpr { protected function doInterpret(InterpreterContext $context, $result_l, $result_r) { $context->replace($this, $result_l || $result_r); } } class BoolAndExpr extends OperatorExpr { protected function doInterpret(InterpreterContext $context, $result_l, $result_r) { $context->replace($this, $result_l && $result_r); } } 




Now having prepared classes and a small system, we are ready to execute a small code. Do you still remember him?

$input equals «8» or $input equals «»



 $context = new InterpreterContext(); $input = new VariableExpr('input'); $stats = new BoolOrExpr(new EqualsExpr($input, new LitralExpr('')), new EqualsExpr($input, new LitralExpr('8'))); foreach (array("", "8", "666") as $val) { $input->setValue($val); print "$val:\n"; $stats->interpret($context); if($context->lookup($stats)) { print '\n\n'; } else { print " \n\n"; } } 




Our system still lacks a parser, you can write your own or take an existing one. For complex systems, it is better to take an existing engine.



image

Interpreter Pattern Issues


Once you have prepared the main classes to implement the Interpreter pattern, it will be easy to expand. The price you have to pay for is only the number of classes you need to create. Therefore, this template is more applicable for relatively large languages. And if you need a full-fledged programming language, it is better to look for tools from third-party companies for this.



Since the Interpreter classes often perform very similar tasks, it is worthwhile to keep track of the classes being created in order to avoid duplication .



Sources:

Gangs of Four

Source: https://habr.com/ru/post/136371/



All Articles