การนำรูปแบบผู้เยี่ยมชมมาใช้สำหรับทรีไวยากรณ์ของบทคัดย่อ


23

ฉันอยู่ในขั้นตอนการสร้างภาษาโปรแกรมของฉันเองซึ่งฉันใช้เพื่อจุดประสงค์ในการเรียนรู้ ฉันเขียน lexer และ parser ที่สืบเชื้อสายแบบเรียกซ้ำสำหรับชุดย่อยของภาษาของฉัน (ปัจจุบันฉันสนับสนุนการแสดงออกทางคณิตศาสตร์เช่น+ - * /และวงเล็บ) โปรแกรมแยกวิเคราะห์ส่งกลับต้นไม้ทรีของฉันซึ่งฉันเรียก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;
    }
}

อย่างไรก็ตามฉันต้องการ decouple อัลกอริทึมจาก tree nodes เพราะฉันต้องการใช้ Open / Closed Principle ดังนั้นฉันไม่จำเป็นต้องเปิดโหนดทุกคลาสอีกครั้งเมื่อฉันต้องการใช้การสร้างรหัสเช่น ฉันอ่านว่ารูปแบบผู้เข้าชมนั้นดีสำหรับสิ่งนั้น ฉันมีความเข้าใจที่ดีเกี่ยวกับรูปแบบการทำงานและการใช้การส่งแบบคู่เป็นวิธีที่จะไป แต่เนื่องจากลักษณะของต้นไม้ที่เกิดซ้ำฉันไม่แน่ใจว่าฉันควรเข้าใกล้มันอย่างไร นี่คือลักษณะที่ผู้เข้าชมของฉันจะมีลักษณะ:

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พารามิเตอร์วิธีการที่ไม่ได้ใช้ในตัวอย่างข้างต้น แต่เพียงฉันเชื่อว่ามันจะเป็นประโยชน์มากทีเดียวที่จะมีหนึ่ง ตัวอย่างเช่นมันอาจเป็นอินสแตนซ์ของคนตัดไม้เพื่อรายงานข้อผิดพลาด


ฉันลงเอยด้วยการนำสิ่งที่คล้ายกันมาใช้และฉันก็พอใจกับผลลัพธ์มาก ขอบคุณ!
marco-fiset

6

ฉันใช้รูปแบบผู้เยี่ยมชมบนต้นไม้แบบเรียกซ้ำมาก่อน

โครงสร้างข้อมูลแบบเรียกซ้ำของฉันนั้นง่ายมาก - มีเพียงสามโหนดเท่านั้น: โหนดทั่วไป, โหนดภายในที่มีลูกและโหนดใบไม้ที่มีข้อมูล นี่ง่ายกว่าที่ฉันคาดไว้ AST ของคุณจะเป็นไปได้

ในกรณีของฉันฉันจงใจไม่ยอมให้ยอมรับโหนดที่มีลูก ๆ เรียกว่ายอมรับกับลูกของมันหรือเพื่อโทรหาผู้เยี่ยมชมเยี่ยมชม (เด็ก) จากภายในยอมรับ มันเป็นความรับผิดชอบของการดำเนินการสมาชิก "เยี่ยมชม" ที่ถูกต้องของผู้เข้าชมเพื่อมอบหมายยอมรับกับเด็ก ๆ ของโหนดที่กำลังเยี่ยมชม ฉันเลือกวิธีนี้เพราะฉันต้องการอนุญาตให้การใช้งานของผู้เข้าชมที่แตกต่างกันสามารถตัดสินใจได้ว่าลำดับของการเยี่ยมชมเป็นอิสระจากการนำเสนอต้นไม้

ประโยชน์รองคือเกือบจะไม่มีสิ่งประดิษฐ์ของรูปแบบผู้เข้าชมในโหนดต้นไม้ของฉัน - "ยอมรับ" แต่ละคนก็เรียก "เยี่ยมชม" ในผู้เข้าชมที่มีรูปแบบที่ถูกต้อง สิ่งนี้ทำให้การค้นหาและทำความเข้าใจกับตรรกะการเยี่ยมชมได้ง่ายขึ้นซึ่งทั้งหมดนี้อยู่ในการดำเนินการของผู้เข้าชม

เพื่อความชัดเจนฉันได้เพิ่ม C ++ - ish pseudocode ก่อนอื่นโหนด:

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

สิ่งนี้อาจล้มเหลวในการแยกวิเคราะห์หากภาษามีโครงสร้างซ้อนกันอย่างลึกล้ำ - มันอาจจำเป็นในการรักษาสแต็กอย่างเป็นอิสระจากสแตกการโทรของภาษา
Pete Kirkham

1
@PeteKirkham: นั่นจะต้องเป็นต้นไม้ที่ค่อนข้างลึก
Robert Harvey

@PeteKirkham คุณหมายถึงอะไรที่จะล้มเหลว คุณหมายถึง StackOverflowException บางอย่างหรือว่าแนวคิดนั้นไม่ได้ดี สำหรับช่วงเวลาที่ฉันไม่สนใจเกี่ยวกับการแสดงฉันทำเพื่อความสนุกสนานและการเรียนรู้เท่านั้น
marco-fiset

@ marco-fiset ใช่คุณจะได้รับข้อยกเว้นสแต็คโอเวอร์โฟลว์หากคุณพูดลองแยกไฟล์ XML ขนาดใหญ่ที่มีผู้เยี่ยมชมออกมา คุณจะสามารถใช้ภาษาโปรแกรมส่วนใหญ่ได้
Pete Kirkham
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.