Реализация шаблона посетителя для абстрактного синтаксического дерева

23

Я нахожусь в процессе создания своего собственного языка программирования, который я делаю для целей обучения. Я уже написал лексер и парсер рекурсивного спуска для подмножества моего языка (в настоящее время я поддерживаю математические выражения, такие как + - * /и круглые скобки). Парсер возвращает мне абстрактное синтаксическое дерево, в котором я вызываю Evaluateметод для получения результата выражения. Все отлично работает Вот примерно моя текущая ситуация (примеры кода на C #, хотя это в значительной степени не зависит от языка):

public abstract class Node
{
    public abstract Double Evaluate();
}

public class OperationNode : Node
{
    public Node Left { get; set; }
    private String Operator { get; set; }
    private Node Right { get; set; }

    public Double Evaluate()
    {
        if (Operator == "+")
            return Left.Evaluate() + Right.Evaluate();

        //Same logic for the other operators
    }
}

public class NumberNode : Node
{
    public Double Value { get; set; }

    public Double Evaluate()
    {
        return Value;
    }
}

Однако я хотел бы отделить алгоритм от узлов дерева, потому что я хочу применить принцип Open / Closed, чтобы мне не пришлось заново открывать каждый класс узлов, когда я хочу реализовать генерацию кода, например. Я читал, что Шаблон посетителя хорош для этого. Я хорошо понимаю, как работает шаблон, и что использование двойной диспетчеризации - это путь. Но из-за рекурсивной природы дерева я не уверен, как мне к нему подойти. Вот как будет выглядеть мой посетитель:

public class AstEvaluationVisitor
{
    public void VisitOperation(OperationNode node)
    {
        // Here is where I operate on the operation node.
        // How do I implement this method?
        // OperationNode has two child nodes, which may have other children
        // How do I work the Visitor Pattern around a recursive structure?

        // Should I access children nodes here and call their Accept method so they get visited? 
        // Or should their Accept method be called from their parent's Accept?
    }

    // Other Visit implementation by Node type
}

Так что это моя проблема. Я хочу немедленно заняться этим, пока мой язык не поддерживает большую функциональность, чтобы избежать более серьезной проблемы в дальнейшем.

Я не опубликовал это в StackOverflow, потому что я не хочу, чтобы вы предоставили реализацию. Я только хочу, чтобы вы поделились идеями и концепциями, которые я, возможно, пропустил, и как я должен подходить к этому.

марко-fiset
источник
1
Я бы, вероятно, вместо этого реализовал сгиб дерева
jk.
@jk .: Не могли бы вы немного уточнить?
Марко-Фисет

Ответы:

10

Реализация посетителя должна решить, посещать ли дочерние узлы и в каком порядке. Вот и весь смысл шаблона посетителя.

Чтобы адаптировать посетителя к большему количеству ситуаций, полезно (и довольно часто) использовать такие обобщения, как это (это Java):

public interface ExpressionNodeVisitor<R, P> {
    R visitNumber(NumberNode number, P p);
    R visitBinary(BinaryNode expression, P p);
    // ...
}

И acceptметод будет выглядеть так:

public interface ExpressionNode extends Node {
    <R, P> R accept(ExpressionNodeVisitor<R, P> visitor, P p);
    // ...
}

Это позволяет передавать дополнительные параметры посетителю и извлекать из него результат. Таким образом, оценка выражения может быть реализована так:

public class EvaluatingVisitor
    implements ExpressionNodeVisitor<Double, Void> {
    public Double visitNumber(NumberNode number, Void p) {
        // Parse the number and return it.
        return Double.valueOf(number.getText());
    }
    public Double visitBinary(BinaryNode binary, Void p) {
        switch (binary.getOperator()) {
        case '+':
            return binary.getLeftOperand().accept(this, p)
                + binary.getRightOperand().accept(this, p);
        // More cases for other operators here.
        }
    }
}

Параметр acceptmethod не используется в приведенном выше примере, но поверьте мне: он очень полезен. Например, это может быть экземпляр Logger, чтобы сообщить об ошибках.

Lorus
источник
В итоге я реализовал нечто подобное, и до сих пор очень доволен результатом. Благодарность!
Марко-Фисет
6

Я реализовал шаблон посетителя на рекурсивном дереве раньше.

Моя конкретная рекурсивная структура данных была предельно простой - всего три типа узлов: общий узел, внутренний узел с дочерними элементами и конечный узел с данными. Это намного проще, чем я предполагаю, что ваш AST будет, но, возможно, идеи могут масштабироваться.

В моем случае я сознательно не позволил узлу Accept узла с детьми вызвать Accept для его потомков или вызвать visitor.Visit (child) изнутри Accept. Правильная реализация посетителя-члена «Посетить» отвечает за делегирование Приемок дочерним узлам посещаемого узла. Я выбрал этот способ, потому что хотел, чтобы разные реализации Visitor могли определять порядок посещения независимо от представления дерева.

Второе преимущество заключается в том, что внутри узлов моего дерева почти нет артефактов шаблона «Посетитель» - каждый «Принять» просто вызывает «Посетить» посетителя с правильным конкретным типом. Это облегчает поиск и понимание логики посещения, это все внутри реализации посетителя.

Для ясности я добавил псевдокод C ++ - ish. Сначала узлы:

class INode {
  public:
    virtual void Accept(IVisitor& i_visitor) = 0;
};

class NodeWithChildren : public INode {
  public:
     virtual void Accept(IVisitor& i_visitor) override {
        i_visitor.Visit(*this);
     }
     // Plus interface for getting the children, exercise for the reader ;-)
 };

 class LeafNode : public INode {
   public:
     virtual void Accept(IVisitor& i_visitor) override {
       i_visitor.Visit(*this);
     }
 };

И посетитель:

class IVisitor {
  public:
     virtual void Visit(NodeWithChildren& i_node) = 0;
     virtual void Visit(LeafNode& i_node) = 0;
};

class ConcreteVisitor : public IVisitor
  public:
     virtual void Visit(NodeWithChildren& i_node) override {
       // Do something useful, then...
       for(Node * p_child : i_node) {
         child->Accept(*this);
       }
     }

     virtual void Visit(LeafNode& i_node) override {
        // Just do something useful, there are no children.
     }

};
Йорис Тиммерманс
источник
1
+1 за allow different Visitor implementations to be able to decide the order of visitation. Очень хорошая идея.
Марко-Фисет
@ marco-fiset Затем алгоритм (посетитель) должен знать, как структурированы данные (узлы). Это сломает алгоритм разделения данных, который дает шаблон посетителя.
B Visschers
2
@BVisschers Посетители реализуют функцию для каждого типа узла, поэтому он знает, на каком узле он работает в любой момент времени. Это ничего не нарушает.
marco-fiset
3

Вы работаете с шаблоном посетителей вокруг рекурсивной структуры так же, как вы делаете что-либо еще с вашей рекурсивной структурой: рекурсивно посещая узлы в вашей структуре.

public class OperationNode
{
    public int SomeProperty { get; set; }
    public List<OperationNode> Children { get; set; }
}

public static void VisitNode(OperationNode node)
{
    ... Visit this node

    foreach(var node in Children)
    {
         VisitNode(node);
    }
}

public static void VisitAllNodes()
{
    VisitNode(rootNode);
}
Роберт Харви
источник
Это может не сработать для анализаторов, если язык имеет глубоко вложенные конструкции - может потребоваться поддержка стека независимо от стека вызовов языка.
Пит Киркхам
1
@PeteKirkham: Это должно быть довольно глубокое дерево.
Роберт Харви
@PeteKirkham Что вы имеете в виду, что он может потерпеть неудачу? Вы имеете в виду какое-то StackOverflowException или что концепция не будет хорошо масштабироваться? На данный момент я не забочусь о производительности, я делаю это только для удовольствия и обучения.
Марко-Фисет
@ marco-fiset Да, вы получаете исключение переполнения стека, если вы говорите, попробуйте проанализировать большой, глубокий XML-файл с посетителем. Вам это сойдет с рук для большинства языков программирования.
Пит Киркхам,