July 7, 2013
Let's say that we have an AST that holds integer nodes. We want to print double the value of all nodes. We can do something like this
class IntegerNode
def initialize(value)
@value = value
end
def double
@value * 2
end
end
class Ast
def initialize
@nodes = []
@nodes << IntegerNode.new(2)
@nodes << IntegerNode.new(3)
end
def print_double
@nodes.each do |node|
puts node.double
end
end
end
ast = Ast.new
ast.print_double # => 4 6
Above solution works. Now let's try to print triple the value. In order to do
that we need to change class IntegerNode
. And IntegerNode
has knowledge of
how to print triple
value. Tomorrow if we have another node called FloatNode
then that node will have knowledge about how to double
and triple
the value.
Nodes are merely storing information. And the representation of data should be
separate from the data itself. So IntegerNode
and FloatNode
should not know
about how to double
and triple
.
To take the data representation code out of nodes we can make use of visitor pattern . Visitor pattern uses double dispatch .
Before we look at "double dispatch" let's first look at "single dispatch".
When we invoke a method in ruby we are using single dispatch. In single
dispatch, method invocation is done based on a single criteria: class of the
object. Most of the object oriented programming languages use single dispatch
system.
In the following case method double
is invoked solely based on the class of
node
.
node.double
As the name suggests in the case of Double dispatch
dispatching depends on two
things: class of the object and the class of the input object.
Ruby inherently does not support "Double dispatch". We will see how to get
around that issue shortly. First let's see an example in Java which support
Double dispatch. Java supports method overloading
which allows two methods
with same name to differ only in the type of argument it receives.
class Node
def double(Integer value); value *2; end
def double(String value); Integer.parseInt(value) * 2; end
end
node.double(2)
node.double("51")
In the above case the method that would be invoked is decided based on two
things: class of the object ( node ) and the class of the value (Integer or
String). That's why this is called Double dispatch
.
In ruby we can't have two methods with same name and different signature because the second method would override the first method. In order to get around that limitation usually the method name has class name. Let's try to write above java code in ruby.
class Node
def accept value
method_name = "visit_#{value.class}"
send method_name
end
def visit_Integer value
value * 2
end
def visit_String value
value.to_i * 2
end
end
If the above code is not very clear then don't worry. We are going to look at visitor pattern in ruby and that will make the above code clearer.
Now let's get back to the problem of traversing the AST. This time we are going to use "Double dispatch" so that node information is separate from representation information.
In visitor pattern nodes
define a method called accept
. That method accepts
the visitor and then that method calls visit
on visitor passing itself as
self.
Below is a concrete example of visitor pattern. You can see that IntegerNode
has method accepts
which takes an instance of visitor
as argument. And then
visit
method of visitor is invoked.
class Node
def accept visitor
raise NotImpelementedError.new
end
end
module Visitable
def accept visitor
visitor.visit self
end
end
class IntegerNode < Node
include Visitable
attr_reader :value
def initialize value
@value = value
end
end
class Ast < Node
def initialize
@nodes = []
@nodes << IntegerNode.new(2)
@nodes << IntegerNode.new(3)
end
def accept visitor
@nodes.each do |node|
node.accept visitor
end
end
end
class DoublerVisitor
def visit subject
puts subject.value * 2
end
end
class TriplerVisitor
def visit subject
puts subject.value * 3
end
end
ast = Ast.new
puts "Doubler:"
ast.accept DoublerVisitor.new
puts "Tripler:"
ast.accept TriplerVisitor.new
# =>
Doubler:
4
6
Tripler:
6
9
Above code used only IntegerNode
. In the next example I have added
StringNode
. Now notice how the visit
method changed. Now based on the class
of the argument the method to dispatch is being decided.
class Node
def accept visitor
raise NotImpelementedError.new
end
end
module Visitable
def accept visitor
visitor.visit(self)
end
end
class IntegerNode < Node
include Visitable
attr_reader :value
def initialize value
@value = value
end
end
class StringNode < Node
include Visitable
attr_reader :value
def initialize value
@value = value
end
end
class Ast < Node
def initialize
@nodes = []
@nodes << IntegerNode.new(2)
@nodes << StringNode.new("3")
end
def accept visitor
@nodes.each do |node|
node.accept visitor
end
end
end
class BaseVisitor
def visit subject
method_name = "visit_#{subject.class}".intern
send(method_name, subject )
end
end
class DoublerVisitor < BaseVisitor
def visit_IntegerNode subject
puts subject.value * 2
end
def visit_StringNode subject
puts subject.value.to_i * 2
end
end
class TriplerVisitor < BaseVisitor
def visit_IntegerNode subject
puts subject.value * 3
end
def visit_StringNode subject
puts subject.value.to_i * 3
end
end
ast = Ast.new
puts "Doubler:"
ast.accept DoublerVisitor.new
puts "Tripler:"
ast.accept TriplerVisitor.new
# =>
Doubler:
4
6
Tripler:
6
9
Arel uses visitor pattern to build query tailored to the specific database. You can see that it has a visitor class for sqlite3, mysql and Postgresql.
If this blog was helpful, check out our full blog archive.