我正在尝试使用Visitor模式构建一个简单的解释器。我很难理解如何使用这种模式来实现诸如漂亮地打印树之类的任务。
我试图获得的结果是使用适当的缩进打印AST:
Expr
'---Abstr
|---Id
'---Expr
'---App
'---Atom
'---Id
我定义了许多表示AST中节点的类:
class ASTNode
attr_reader :children, :pos
def initialize(children, pos)
@children = children
@pos = pos
end
def accept(visitor)
visitor.visit(self)
@children.each { |child| child.accept(visitor) } unless @children.nil?
end
end
class ExprNode < ASTNode
def initialize(children, pos)
super(children, pos)
end
end
...
和执行双重调度的基本访问者类:
class Visitor
def visit(subject)
method_name = "visit_#{subject.class}".intern
send(method_name, subject)
end
end
最后,打印AST的访问者:
class PrintVisitor < Visitor
def visit_ExprNode(subject)
end
def visit_AbstrNode(subject)
end
...
end
答案 0 :(得分:1)
访问者模式有两种版本:一种版本只负责双重调度,另一种版本通过自动访问节点的子代来解决迭代。后一种版本的灵活性较差,因为您可以提前确定所需的遍历类型(预定或后置),而不是将决策权留给单个访问者。它还迫使您只访问所有节点一次(在许多情况下,例如在实现AST解释器时,您将不需要访问该节点)。
在您的代码中,您实际上正在实现这两个版本:您的Visitor#visit
方法实现了普通的访问者模式,而ASTNode#accept
实现了带有迭代的模式。这是accept
方法的怪异用法,因为通常accept方法的工作只是在访问者上调用特定的visit
方法(例如visit_whatever
),以使双重分发工作。由于您已经使用反射来实现双重调度,因此根本不需要accept
方法。
我假定应该在PrintVisitor的visit_ * Node(subject)方法中实现打印
是的。
打印每个节点需要附加的上下文以确定正确的缩进级别。
也正确。您可以通过将缩进级别存储在实例变量中来跟踪缩进级别。然后,给定的访问者方法将以给定的缩进量打印其内容,增加缩进级别,访问其子注释,然后再次减小缩进。像这样:
def visit_SomeNode(some_node)
puts "#{@indent * " "}---SomeNode"
@indent += 4
some_node.children.each {|child| visit(child)}
@indent -= 4
end
您还可以将some_node.children.each {|child| visit(child)}
放入其自己的visit_children(node)
方法中,仅在要对所有子项执行相同操作的情况下调用该方法(如上所述)。
如果要避免该可变状态,还可以调整访问者类,以允许将参数传递给visit
,如下所示:
class Visitor
def visit(subject, *args)
method_name = "visit_#{subject.class}".intern
send(method_name, subject, *args)
end
end
然后,您可以在方法中添加缩进级别的参数,并在访问孩子时将增加的缩进级别传递给visit
。