Implementieren des Besuchermusters für einen abstrakten Syntaxbaum

23

Ich bin dabei, meine eigene Programmiersprache zu erstellen, die ich zu Lernzwecken verwende. Ich habe bereits das Lexer und ein rekursives Descent-Parser für eine Teilmenge meiner Sprache geschrieben (derzeit unterstütze ich mathematische Ausdrücke wie + - * /und in Klammern). Der Parser gibt mir einen abstrakten Syntaxbaum zurück, auf dem ich die EvaluateMethode aufrufe, um das Ergebnis des Ausdrucks zu erhalten. Alles funktioniert gut. Hier ist ungefähr meine aktuelle Situation (Codebeispiele in C #, obwohl dies ziemlich sprachunabhängig ist):

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

Ich möchte den Algorithmus jedoch von den Baumknoten entkoppeln, da ich das Open / Closed-Prinzip anwenden möchte, damit ich nicht jede Knotenklasse erneut öffnen muss, wenn ich beispielsweise die Codegenerierung implementieren möchte. Ich habe gelesen, dass das Besuchermuster dafür gut ist. Ich habe ein gutes Verständnis dafür, wie das Muster funktioniert, und die Verwendung von Doppelversand ist der richtige Weg. Aber aufgrund der rekursiven Natur des Baumes bin ich mir nicht sicher, wie ich es angehen soll. So würde mein Besucher aussehen:

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
}

Das ist also mein Problem. Ich möchte es sofort angehen, während meine Sprache nicht viele Funktionen unterstützt, um später ein größeres Problem zu vermeiden.

Ich habe dies nicht in StackOverflow gepostet, weil ich nicht möchte, dass Sie eine Implementierung bereitstellen. Ich möchte nur, dass Sie Ideen und Konzepte mitteilen, die ich möglicherweise übersehen habe und wie ich damit umgehen sollte.

Marco-Fiset
quelle
1
Ich würde wahrscheinlich stattdessen eine
Baumfalte
@jk .: Würde es Ihnen etwas ausmachen, etwas näher darauf einzugehen?
Marco-Fiset

Antworten:

10

Es liegt an der Besucherimplementierung, zu entscheiden, ob und in welcher Reihenfolge untergeordnete Knoten besucht werden sollen. Das ist der springende Punkt des Besuchermusters.

Um den Besucher an weitere Situationen anzupassen, ist es hilfreich (und durchaus üblich), solche Generika zu verwenden (es ist Java):

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

Und eine acceptMethode würde so aussehen:

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

Dadurch können zusätzliche Parameter an den Besucher übergeben und ein Ergebnis daraus abgerufen werden. Die Ausdrucksauswertung kann also folgendermaßen implementiert werden:

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

Der acceptmethod-Parameter wird im obigen Beispiel nicht verwendet, aber glauben Sie mir: Es ist sehr nützlich, einen zu haben. Beispielsweise kann es sich um eine Logger-Instanz handeln, an die Fehler gemeldet werden.

lorus
quelle
Am Ende habe ich etwas Ähnliches implementiert und bin mit dem bisherigen Ergebnis sehr zufrieden. Vielen Dank!
Marco-Fiset
6

Ich habe das Besuchermuster zuvor in einem rekursiven Baum implementiert.

Meine spezielle rekursive Datenstruktur war extrem einfach - nur drei Knotentypen: der generische Knoten, ein interner Knoten mit untergeordneten Knoten und ein Blattknoten mit Daten. Dies ist viel einfacher als ich es von Ihrem AST erwarte, aber vielleicht können die Ideen skalieren.

In meinem Fall habe ich absichtlich nicht zugelassen, dass die Annahme eines Knotens mit Kindern die Annahme für seine Kinder aufruft oder den Besucher aufruft. Besuchen Sie (Kind) innerhalb der Annahme. Es liegt in der Verantwortung der korrekten Implementierung des Besuchers als "Besuch" -Mitglied, die Annahme an Kinder des besuchten Knotens zu delegieren. Ich habe mich für diesen Weg entschieden, weil ich verschiedenen Visitor-Implementierungen erlauben wollte, die Reihenfolge der Besuche unabhängig von der Baumdarstellung zu bestimmen.

Ein zweiter Vorteil ist, dass in meinen Baumknoten fast keine Artefakte des Besuchermusters vorhanden sind - jedes "Annehmen" ruft beim Besucher nur "Besuch" mit dem richtigen konkreten Typ auf. Dies macht es einfacher, die Besuchslogik zu lokalisieren und zu verstehen, alles ist in der Besucherimplementierung enthalten.

Der Klarheit halber habe ich einen C ++ - ischen Pseudocode hinzugefügt. Zuerst die Knoten:

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

Und der Besucher:

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

};
Joris Timmermans
quelle
1
+1 für allow different Visitor implementations to be able to decide the order of visitation. Sehr gute Idee.
Marco-Fiset
@ marco-fiset Der Algorithmus (Besucher) muss dann wissen, wie die Daten (Knoten) strukturiert sind. Dadurch wird die vom Besuchermuster vorgegebene Trennung zwischen Algorithmus und Daten aufgeschlüsselt.
B Visschers
2
@BVisschers Die Besucher implementieren eine Funktion für jeden Knotentyp, sodass sie wissen, auf welchem ​​Knoten sie zu einem bestimmten Zeitpunkt arbeiten. Es zerbricht nichts.
Marco-Fiset
3

Sie arbeiten das Besuchermuster um eine rekursive Struktur herum, wie Sie es auch mit Ihrer rekursiven Struktur tun würden: indem Sie die Knoten in Ihrer Struktur rekursiv besuchen.

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);
}
Robert Harvey
quelle
Dies kann für Parser fehlschlagen, wenn die Sprache tief verschachtelte Konstrukte enthält. Es kann erforderlich sein, einen Stapel unabhängig vom Aufrufstapel der Sprache zu verwalten.
Pete Kirkham
1
@PeteKirkham: Das müsste ein ziemlich tiefer Baum sein.
Robert Harvey
@PeteKirkham Was meinst du damit, dass es scheitern kann? Meinen Sie damit eine Art StackOverflowException oder dass das Konzept nicht gut skalierbar ist? Im Moment ist mir Leistung egal, ich mache das nur zum Spaß und zum Lernen.
Marco-Fiset
@ marco-fiset Ja, Sie erhalten eine Stapelüberlauf-Ausnahme, wenn Sie sagen, dass Sie versuchen, eine große, tiefe XML-Datei mit einem Besucher zu analysieren. Mit den meisten Programmiersprachen kommen Sie zurecht.
Pete Kirkham