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


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;
    }
}

Однак я хотів би роз'єднати алгоритм від вузлів дерева, тому що я хочу застосувати відкритий / закритий принцип, тому мені не доведеться відкривати кожен клас вузлів, коли я хочу реалізувати генерацію коду, наприклад. Я читав, що шаблон для відвідувачів хороший для цього. Я добре розумію, як працює схема, і що використання подвійної відправки - це шлях. Але через рекурсивний характер дерева, я не впевнений, як я повинен підходити до нього. Ось як виглядатиме мій Відвідувач:

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, тому що я не хочу, щоб ви надавали імплементацію. Я хочу лише, щоб ти поділився ідеями та концепціями, які я, можливо, пропустив, і як я повинен до цього підходити.


1
Я, мабуть, натомість реалізує складку дерева
jk.

@jk. Ви б проти зауважити трохи?
marco-fiset

Відповіді:


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.
        }
    }
}

Параметр acceptметоду не використовується у наведеному вище прикладі, але просто повірте: його досить корисно. Наприклад, повідомлення про помилки може бути екземпляром Logger.


Я закінчив реалізувати щось подібне, і я дуже задоволений результатом поки що. Спасибі!
marco-fiset

6

Я раніше реалізував шаблон відвідувачів на дереві-рекурсиві.

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

У моєму випадку я навмисно не дозволив Accept вузлу з дітьми викликати Accept на своїх дітей, або зателефонувати відвідувачу. Відвідайте (дитина) зсередини Accept. Відповідальність за правильну реалізацію відвідувачем програми "Відвідати" є делегуванням Accepts дітям вузла, який відвідується. Я вибрав цей шлях, тому що хотів дозволити різним реалізаціям відвідувачів мати можливість визначати порядок відвідування незалежно від представлення дерева.

Вторинна перевага полягає в тому, що майже немає артефактів шаблону відвідувачів всередині моїх деревних вузлів - кожен "Прийняти" просто викликає "Відвідати" відвідувача з правильним конкретним типом. Це полегшує пошук і розуміння логіки відвідування, це все, що знаходиться всередині відвідувача.

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

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

@ marco-fiset Алгоритм (відвідувач) повинен буде знати, як структуруються дані (вузли). Це дозволить розбити алгоритм поділу даних, який дає шаблон відвідувача.
B Вишчери

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

@ marco-fiset Так, ви отримаєте виняток переповнення стека, якщо скажете, спробуйте проаналізувати великий, глибокий XML-файл із відвідувачем. Ви зникнете з нею для більшості мов програмування.
Піт Кіркхем
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.