为抽象语法树实现访问者模式


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-set

Answers:


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 2013年

6

我以前在递归树上实现了访问者模式。

我特殊的递归数据结构非常简单-仅有三种节点类型:通用节点,具有子级的内部节点和具有数据的叶节点。这比我预期的AST简单得多,但也许可以扩展。

在我的情况下,我故意不让具有子节点的“接受”对其子节点调用“接受”,也没有从“接受”内部调用visitor.Visit(child)。访问者的正确“访问”成员实现有责任将“接受”委派给被访问节点的子代。我之所以选择这种方式,是因为我想允许不同的Visitor实现能够独立于树表示来决定访问顺序。

第二个好处是,我的树节点内几乎没有访客模式的伪像-每个“接受”仅使用正确的具体类型调用访客上的“访问”。这使得查找和理解访问逻辑变得更加容易,所有这些都在访问者实现内部。

为了清楚起见,我添加了一些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 2013年

@ marco-fiset然后,算法(访问者)将必须知道数据(节点)的结构。这将破坏访客模式给出的算法数据分离。
B Visschers 2014年

2
@BVisschers访客为每种节点类型实现一个功能,因此它知道它在任何给定时间在哪个节点上运行。它不会破坏任何东西。
marco-fiset 2014年

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:那一定是一棵很深的树。
罗伯特·哈维

@PeteKirkham你什么意思会失败?您是说某种StackOverflowException还是该概念无法很好地扩展?目前,我并不关心性能,我只是为了娱乐和学习而这样做。
marco-set

@ marco-fiset是的,如果您说到堆栈溢出异常,请尝试与访问者一起解析大型的深XML文件。对于大多数编程语言,您都将摆脱它。
Pete Kirkham
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.