Pattern 10Replacing Visitor

Intent

To encapsulate an action to be performed on a data structure in a way that allows the addition of new operations to the data structure without having to modify it.

Overview

A common sticking point in large, long-lived programs is how to extend a data type. We want to extend along two dimensions. First, we may want to add new operations to existing implementations of the data type. Second, we may want to add new implementations of the data type.

We’d like to be able to do this without recompiling the original source, indeed, possibly without even having access to it. This is a problem that’s as old as programming itself, and it’s now known as the expression problem.

For example, consider Java’s Collection as a sample data type. The Collection interface defines many methods, or operations, and has many implementations. In a perfect world, we’d be able to easily add both new operations to Collection as well as new implementations of Collection.

In object-oriented languages, however, it’s only easy to do the latter. We can create a new implementation of Collection by implementing the interface. If we want to add new operations to Collection that work with all the existing Collection implementation, we’re out of luck.

In Java, we often get around this by creating a class full of static utility methods, rather than by adding the operations directly to the data type. One such library for Collection is the Apache foundation’s CollectionUtils.

Visitor is another partial solution to this sort of problem. It allows us to add new operations to an existing data type and is often used with tree-structured data. Visitor allows us to fairly easily add new operations to an existing data type, but it makes adding new implementations of the data type difficult.

Visitor Pattern

The Visitor class diagram (shown in the following figure) shows the main pieces of the Visitor pattern. Our data type here is the DataElement class, which has two implementations. Instead of implementing operations directly on the subclasses of DataElement, we create an accept method that takes a Visitor and calls visit, passing itself in.

images/Visitor.png

Figure 8. Visitor Classes. A sketch of the Visitor pattern

This inverts the normal object-oriented constraint that it’s easy to add new implementations of a data type but difficult to add new operations. If we want to add a new operation, we just need to create a new visitor and write code such that it knows how to visit each existing concrete element.

However, it’s hard to add new implementations of DataElement. To do so, we’d need to modify all of the existing visitors to know how to visit the new DataElement implementation. If those Visitor classes are outside of our control, it may be impossible!

Functional Replacement

The Visitor pattern makes it possible to add new operations to an object-oriented data type but difficult, or impossible, to add new implementations of the type. In the functional world, this is the norm. It’s easy to add a new operation on some data type by writing a new function that operates on it, but it’s difficult to add new data types to an existing operation.

In our replacements, we’ll examine a few different ways to deal with this extensibility problem in Scala and Clojure. The solutions are quite different in the two languages. In part, this is because Scala is statically typed while Clojure is dynamically typed. This means that Scala has a harder problem to solve in that it attempts to perform its extensions while preserving static type safety.

The other difference is that Scala’s take on polymorphism is an extension of the traditional object-oriented model, which uses a hierarchy of subclasses. Clojure takes a novel view and provides polymorphism in a more ad hoc manner. Since polymorphism is intimately bound up with extensibility, this affects the overall shape of the solutions.

In Scala

Since Scala is a hybrid language, extending existing code requires us to dip into its object-oriented features, especially its type system.

First we’ll look at a method of extending the operations in an existing library that uses Scala’s implicit conversion system. This allows us to add new operations to existing libraries.

Second we’ll look at a solution that takes advantage of Scala’s mix-in inheritance and traits, which allows us to easily add both new operations and new implementations to a data type.

In Clojure

In Clojure we’ll take a look at the language’s unique take on polymorphism. First we’ll look at Clojure’s datatypes and protocols. These allow us to specify data types and the operations performed on them independently and to extend datatypes with both new implementations and new operations while taking advantage of the JVMs highly optimized method dispatch.

Next we’ll look at Clojure’s multimethods. These allow us to provide our own dispatch function, which lets us dispatch a method call however we please. They’re more flexible than protocols but slower, since they require an additional function call to the user-provided dispatch function.

The Scala and Clojure solutions we examine aren’t exactly equivalent, but they both provide flexible ways to extend existing code.

Sample Code: Extensible Persons

In this example, we’ll look at a Person type and see how we can extend it to have both new implementations and operations. This doesn’t replace the full Visitor pattern, but it’s a simpler example of the sorts of problems that Visitor touches on.

In Java

The code that we’ll look at here is a basic example of extending an existing library without wrapping the original objects. In Java, it would be easy to create new implementations of a Person type, assuming the original libraries’ authors defined an interface for it.

More difficult would be adding new operations to Person. We can’t just create a subinterface of Person with new methods, as that could no longer be used in place of a plain Person. Wrapping Person in a new class is also out for the same reason.

Java doesn’t have a good story for extending an existing type to have new operations, so we often end up faking it by creating classes full of static utility methods that operate on the type. Scala and Clojure give us more flexibility to extend along both dimensions.

In Scala

In Scala, our Person is defined by a trait. The trait specifies methods to get a person’s first name, last name, house number, and street. In addition, there’s a method to get the person’s full name, as the following code shows:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/visitor/Examples.scala
 
trait​ Person {
 
def​ fullName: ​String
 
def​ firstName: ​String
 
def​ lastName: ​String
 
def​ houseNum: ​Int
 
def​ street: ​String
 
}

Now let’s create an implementation of our Person type, SimplePerson. We’ll take advantage of the fact that Scala will automatically create methods that expose the attributes passed into a constructor. The only method we need to implement by hand is fullName, as the following code snippet shows:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/visitor/Examples.scala
 
class​ SimplePerson(​val​ firstName: ​String​, ​val​ lastName: ​String​,
 
val​ houseNum: ​Int​, ​val​ street: ​String​) ​extends​ Person {
 
def​ fullName = firstName + ​" "​ + lastName
 
}

Now we can create a SimplePerson and call the fullName method:

 
scala>​ val simplePerson = new SimplePerson("Mike", "Linn", 123, "Fake. St.")
 
simplePerson: com.mblinn.mbfpp.oo.visitor.Examples.SimplePerson = ...
 
scala>​ simplePerson.fullName
 
res0: String = Mike Linn

What if we want to extend the Person type to have another operation, fullAddress? One way to do so would be to simply create a new subtype with the new operation, but then we couldn’t use that new type where a Person is needed.

In Scala a better way is to define an implicit conversion that converts from a Person to a new class with the fullAddress method. An implicit conversion changes from one type to another depending on context.

Most languages have a certain set of explicit conversions, or casts, built in. For instance, if you use the + operator on an int and a String in Java, the int will be converted to a String and the two will be concatenated.

Scala lets programmers define their own implicit conversions. One way to do so is by using an implicit class. An implicit class exposes its constructor as a candidate for implicit conversions. The following code snippet creates an implicit class that converts from a Person to an ExtendedPerson with a fullAddress:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/visitor/Examples.scala
 
implicit​ ​class​ ExtendedPerson(person: Person) {
 
def​ fullAddress = person.houseNum + ​" "​ + person.street
 
}

Now when we try to call fullAddress on a Person, the Scala compiler will realize that the Person type has no such method. It will then search for an implicit conversion from a Person to a type that does and find it in the ExtendedPerson class.

The compiler will then construct an ExtendedPerson by passing the Person into its primary constructor and call fullAddress on it, as the following REPL output demonstrates:

 
scala>​ simplePerson.fullAddress
 
res1: String = 123 Fake. St.

Now that we’ve seen the trick that allows us to simulate adding new methods to an existing type, the hard part is done. Adding a new implementation of the type is as simple as creating a new implementation of the original Person trait.

Let’s take a look at a Person implementation called ComplexPerson that uses separate objects for its name and its address:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/visitor/Examples.scala
 
class​ ComplexPerson(name: Name, address: Address) ​extends​ Person {
 
def​ fullName = name.firstName + ​" "​ + name.lastName
 
 
def​ firstName = name.firstName
 
def​ lastName = name.lastName
 
def​ houseNum = address.houseNum
 
def​ street = address.street
 
}
 
class​ Address(​val​ houseNum: ​Int​, ​val​ street: ​String​)
 
class​ Name(​val​ firstName: ​String​, ​val​ lastName: ​String​)

Now we create a new ComplexPerson:

 
scala>​ val name = new Name("Mike", "Linn")
 
name: com.mblinn.mbfpp.oo.visitor.Examples.Name = ..
 
 
scala>​ val address = new Address(123, "Fake St.")
 
address: com.mblinn.mbfpp.oo.visitor.Examples.Address = ..
 
 
scala>​ val complexPerson = new ComplexPerson(name, address)
 
complexPerson: com.mblinn.mbfpp.oo.visitor.Examples.ComplexPerson = ...

Our existing implicit conversion will still work!

 
scala>​ complexPerson.fullName
 
res2: String = Mike Linn
 
 
scala>​ complexPerson.fullAddress
 
res3: String = 123 Fake St.

This means we were able to extend a data type with both a new operation and a new implementation.

In Clojure

Let’s take a look at our extensible persons example in Clojure. We’ll start by defining a protocol with a single operation in it, extract-name. This operation is intended to extract a full name out of a person and is defined in the following code snippet:

ClojureExamples/src/mbfpp/oo/visitor/examples.clj
 
(​defprotocol​ NameExtractor
 
(extract-name [this] ​"Extracts a name from a person."​))

Now we can create a Clojure record, SimplePerson, using defrecord. This creates a data type with several fields on it:

ClojureExamples/src/mbfpp/oo/visitor/examples.clj
 
(​defrecord​ SimplePerson [first-name last-name house-num street])

We can create a new instance of a SimplePerson using the ->SimplePerson factory function, as we do in the following snippet:

 
=> (def simple-person (->SimplePerson "Mike" "Linn" 123 "Fake St."))
 
#'mbfpp.oo.visitor.examples/simple-person

Once created, we can get at fields in the data type as if it were a map with keywords for keys. In the following snippet, we get the first name out of our simple person instance:

 
=> (:first-name simple-person)
 
"Mike"

Notice how we defined our data type and the set of operations independently? To hook the two together, we can use extend-type to have our SimplePerson implement the NameExtractor protocol, as we do in the following snippet:

ClojureExamples/src/mbfpp/oo/visitor/examples.clj
 
(​extend-type​ SimplePerson
 
NameExtractor
 
(extract-name [this]
 
(​str​ (:first-name this) ​" "​ (:last-name this))))

Now we can call extract-name on a SimplePerson and have it extract the person’s full name:

 
=> (extract-name simple-person)
 
"Mike Linn"

Now let’s see how to create a new type, ComplexPerson, which represents its name and address as an embedded map. We’ll use a version of defrecord that allows us to extend the type to a protocol at the same time we create it. This is just a convenience; the record and protocol that we’ve created are still their own entities:

ClojureExamples/src/mbfpp/oo/visitor/examples.clj
 
(​defrecord​ ComplexPerson [​name​ address]
 
NameExtractor
 
(extract-name [this]
 
(​str​ (​->​ this :name :first) ​" "​ (​->​ this :name :last))))

Now we can create a ComplexPerson and extract its full name:

 
=> (def complex-person (->ComplexPerson {:first "Mike" :last "Linn"}
 
{:house-num 123 :street "Fake St."}))
 
#'mbfpp.oo.visitor.examples/complex-person
 
=> (extract-name complex-person)
 
"Mike Linn"

To add a new operation or set of operations to our existing types, we only need to create a new protocol and extend the types. In the following snippet, we create a protocol that allows us to extract an address from a person:

ClojureExamples/src/mbfpp/oo/visitor/examples.clj
 
(​defprotocol
 
AddressExtractor
 
(extract-address [this] ​"Extracts and address from a person."​))

Now we can extend our existing types to conform to the new protocol, as we do in the following code:

ClojureExamples/src/mbfpp/oo/visitor/examples.clj
 
(​extend-type​ SimplePerson
 
AddressExtractor
 
(extract-address [this]
 
(​str​ (:house-num this) ​" "​ (:street this))))
 
 
(​extend-type​ ComplexPerson
 
AddressExtractor
 
(extract-address [this]
 
(​str​ (​->​ this :address :house-num)
 
" "
 
(​->​ this :address :street))))

As we can see from the following REPL output, both of our datatypes now conform to the new protocol:

 
=> (extract-address complex-person)
 
"123 Fake St."
 
=> (extract-address simple-person)
 
"123 Fake St."

While we’ve used Scala’s implicit conversions and Clojure protocols to achieve a similar end here, they’re not the same. In Scala, the operations we saw were methods defined on classes, which are part of a type. Scala’s implicit conversion technique allows us to implicitly convert from one type to another, which makes it look as if we can add operations to an existing type.

Clojure’s protocols, on the other hand, define sets of operations and types completely independently via protocols and records. We can then extend any record with any number of protocols, which allows us to easily extend an existing solution both with new operations and new types.

Sample Code: Extensible Geometry

Let’s take a look at a more involved example. We’ll start off by defining two shapes, a circle and a rectangle, and an operation that calculates their perimeters.

Then we’ll show how we can independently add new shapes that work with the existing perimeter operation and new operations that work with our existing shapes. Finally, we’ll show how to combine both types of extensions.

In Java

In Java, this is a problem that’s impossible to solve well. Extending the shape type to have additional implementations is easy. We create a Shape interface with multiple implementations.

If we want to extend Shape so that it has new methods, it’s a bit more difficult, but we can use Visitor as demonstrated in the Visitor Classes diagram.

images/ShapeVisitor.png

Figure 9. Shape Visitor. The Visitor pattern implemented

However, if we go this route, it’s now difficult to have new implementations because we’d have to modify all of the existing Visitors. If the Visitors are implemented by third-party code, it can be impossible to extend in this dimension without introducing backwards-incompatible changes.

In Java, we need to decide at the outset whether we want to add new operations over our Shape or whether we want new implementations of it.

In Scala

In Scala, we use a simplified version of a technique introduced in a paper written by Scala’s designer, Martin Odersky.

We’ll create a trait, Shape, to serve as the base for all of our shapes. We’ll start off with a single method, perimeter, and two implementations, Circle and Rectangle.

To perform our extension magic, we’ll use some advanced features of Scala’s type system. First, we’ll take advantage of the fact that we can use Scala’s traits as modules. At each step, we’ll package our code in a top-level trait separate from the one we’re using to represent Shape.

This allows us to bundle sets of data types and operations together and to extend those bundles later on using Scala’s mix-in inheritance. Then we can have a new type extend many different traits, an ability we take advantage of to combine independent extensions.

Let’s dig into the code, starting with our initial Shape trait and the first two implementations:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/visitor/Shapes.scala
 
trait​ PerimeterShapes {
 
trait​ Shape {
 
def​ perimeter: Double
 
}
 
 
class​ Circle(radius: Double) ​extends​ Shape {
 
def​ perimeter = 2 * Math.PI * radius
 
}
 
 
class​ Rectangle(width: Double, height: Double) ​extends​ Shape {
 
def​ perimeter = 2 * width + 2 * height
 
}
 
}

Outside of the top-level PerimeterShapes trait, this is a pretty straightforward declaration of a Shape trait and a couple of implementations. To use our shape code we can extend an object with the top-level trait.

This adds our Shape trait and its implementations to the object. We can now use them directly or easily import them into the REPL, as we do in the following code:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/visitor/Shapes.scala
 
object​ FirstShapeExample ​extends​ PerimeterShapes {
 
val​ aCircle = ​new​ Circle(4);
 
val​ aRectangle = ​new​ Rectangle(2, 2);
 
}

Now we can import our shapes into the REPL and try them out, like in the following snippet:

 
import com.mblinn.mbfpp.oo.visitor.FirstShapeExample._
 
 
scala>​ aCircle.perimeter
 
res1: Double = 25.132741228718345
 
 
scala>​ aRectangle.perimeter
 
res2: Double = 8.0

Extending our Shape with new operations is what’s difficult in most purely object-oriented languages, so let’s tackle that first. To extend our initial set of shapes, we create a new top-level trait called AreaShapes, which extends PerimeterShapes.

Inside of AreaShapes we extend our initial Shape class to have an area method, and we create a new Circle and a new Rectangle, which implement area. The code for our extensions follows:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/visitor/Shapes.scala
 
trait​ AreaShapes ​extends​ PerimeterShapes {
 
trait​ Shape ​extends​ ​super​.Shape {
 
def​ area: Double
 
}
 
 
class​ Circle(radius: Double) ​extends​ ​super​.Circle(radius) ​with​ Shape {
 
def​ area = Math.PI * radius * radius
 
}
 
 
class​ Rectangle(width: Double, height: Double)
 
extends​ ​super​.Rectangle(width, height) ​with​ Shape {
 
def​ area = width * height
 
}
 
}

Let’s take a look at this in greater detail. First we create our top-level trait AreaShapes, which extends PerimeterShapes. This lets us easily refer to and extend the classes and trait inside of AreaShapes:

 
trait​ AreaShapes ​extends​ PerimeterShapes {
 
area-shapes
 
}

Next we create a new Shape trait inside of AreaShapes and have it extend the old one inside of PerimeterShapes:

 
trait​ Shape ​extends​ ​super​.Shape {
 
def​ area: Double
 
}

We need to refer to the Shape class in PerimeterShapes as super.Shape to differentiate it from the one we just created in AreaShapes.

Now we’re ready to implement our area. To so we first extend our old Circle and Rectangle classes, and then we mix in our new Shape trait, which has area on it.

Finally, we implement the area on our new Circle and Rectangle, as shown in the following snippet:

 
class​ Circle(radius: Double) ​extends​ ​super​.Circle(radius) ​with​ Shape {
 
def​ area = Math.PI * radius * radius
 
}
 
 
class​ Rectangle(width: Double, height: Double)
 
extends​ ​super​.Rectangle(width, height) ​with​ Shape {
 
def​ area = width * height
 
}

Now we can create some sample shapes and see both perimeter and area in action:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/visitor/Shapes.scala
 
object​ SecondShapeExample ​extends​ AreaShapes {
 
val​ someShapes = Vector(​new​ Circle(4), ​new​ Rectangle(2, 2));
 
}
 
scala>​ for(shape <- someShapes) yield shape.perimeter
 
res0: scala.collection.immutable.Vector[Double] = Vector(25.132741228718345, 8.0)
 
 
scala>​ for(shape <- someShapes) yield shape.area
 
res1: scala.collection.immutable.Vector[Double] = Vector(50.26548245743669, 4.0)

That covers the hard part, extending Shape with a new operation. Now let’s take a look at the easier part. We’ll extend Shape to have a new implementation by creating a Square class.

In the first piece of our extension we create the MorePerimeterShapes top-level trait, which extends the original PerimeterShapes. Inside, we create a new Square implementation of our original trait class. The first piece of our extension is in the following code:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/visitor/Shapes.scala
 
trait​ MorePerimeterShapes ​extends​ PerimeterShapes {
 
class​ Square(side: Double) ​extends​ Shape {
 
def​ perimeter = 4 * side;
 
}
 
}

Now we can create another new top-level trait, MoreAreaShapes, that extends our original AreaShapes and mixes in the MorePerimeterShapes trait we just created. Inside this trait, we extend the Square we just created to also have an area method:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/visitor/Shapes.scala
 
trait​ MoreAreaShapes ​extends​ AreaShapes ​with​ MorePerimeterShapes {
 
class​ Square(side: Double) ​extends​ ​super​.Square(side) ​with​ Shape {
 
def​ area = side * side
 
}
 
}

Now we can add a Square to our test shapes and see the full set of shapes and operations in action, as we do in the following code:

ScalaExamples/src/main/scala/com/mblinn/mbfpp/oo/visitor/Shapes.scala
 
object​ ThirdShapeExample ​extends​ MoreAreaShapes {
 
val​ someMoreShapes = Vector(​new​ Circle(4), ​new​ Rectangle(2, 2), ​new​ Square(4));
 
}
 
scala>​ for(shape <- someMoreShapes) yield shape.perimeter
 
res2: scala.collection.immutable.Vector[Double] =
 
Vector(25.132741228718345, 8.0, 16.0)
 
 
scala>​ for(shape <- someMoreShapes) yield shape.area
 
res3: scala.collection.immutable.Vector[Double] =
 
Vector(50.26548245743669, 4.0, 16.0)

Now we’ve successfully added both new implementations of Shape and new operations over it, and we’ve done so in a typesafe manner!

In Clojure

Our Clojure solution relies on multimethods, which let us specify an arbitrary dispatch function. Let’s take a look at a simple example.

First, we create the multimethod using defmulti. This doesn’t specify any implementations of the method; rather, it contains a dispatch function. In the following snippet we create a multimethod named test-multimethod. The dispatch function is a function of one argument, and it returns that argument untouched. However, it can be an arbitrary piece of code:

ClojureExamples/src/mbfpp/oo/visitor/examples.clj
 
(​defmulti​ test-multimethod (​fn​ [​keyword​] ​keyword​))

The multimethod is implemented using defmethod. Method definitions look much like function definitions, except that they also contain a dispatching value, which corresponds to the values returned from the dispatch function.

In the following snippet, we define two implementations of test-multimethod. The first expects a dispatch value of :foo, and the second, :bar.

ClojureExamples/src/mbfpp/oo/visitor/examples.clj
 
(​defmethod​ test-multimethod :foo [a-map]
 
"foo-method was called"​)
 
 
(​defmethod​ test-multimethod :bar [a-map]
 
"bar-method was called"​)

When the multimethod is called, the dispatch function is first called, and then Clojure dispatches the call to the method with the matching dispatch value. Since our dispatch function returns its input, we call it with the desired dispatch values. The following REPL output demonstrates this:

 
=> (test-multimethod :foo)
 
"foo-method was called"
 
=> (test-multimethod :bar)
 
"bar-method was called"

Now that we’ve seen a basic example of multimethods in action, let’s dig a bit deeper in. We’ll define our perimeter operation as a multimethod. The dispatch function expects a map that represents our shape. One of the keys in the map is :shape-name, which the dispatch function extracts as our dispatch value.

Our perimeter multimethod is defined below, along with implementations for the circle and the rectangle:

ClojureExamples/src/mbfpp/oo/visitor/examples.clj
 
(​defmulti​ perimeter (​fn​ [shape] (:shape-name shape)))
 
(​defmethod​ perimeter :circle [circle]
 
(​*​ 2 Math/PI (:radius circle)))
 
(​defmethod​ perimeter :rectangle [rectangle]
 
(​+​ (​*​ 2 (:width rectangle)) (​*​ 2 (:height rectangle))))

Now we can define a few test shapes:

ClojureExamples/src/mbfpp/oo/visitor/examples.clj
 
(​def​ some-shapes [{:shape-name :circle :radius 4}
 
{:shape-name :rectangle :width 2 :height 2}])

Then we can run our perimeter method over them:

 
=> (for [shape some-shapes] (perimeter shape))
 
(25.132741228718345 8)

To add new operations, we create a new multimethod that handles the existing dispatch values. In the following snippet, we add support for an area operation:

ClojureExamples/src/mbfpp/oo/visitor/examples.clj
 
(​defmulti​ area (​fn​ [shape] (:shape-name shape)))
 
(​defmethod​ area :circle [circle]
 
(​*​ Math/PI (:radius circle) (:radius circle)))
 
(​defmethod​ area :rectangle [rectangle]
 
(​*​ (:width rectangle) (:height rectangle)))

Now we can calculate an area for our shapes as well:

 
=> (for [shape some-shapes] (area shape))
 
(50.26548245743669 4)

To add a new shape into the set of shapes we can handle across both the perimeter and area operations, we add new implementations of our multimethods that handle the appropriate dispatch values. In the following code, we add support for squares:

ClojureExamples/src/mbfpp/oo/visitor/examples.clj
 
(​defmethod​ perimeter :square [square]
 
(​*​ 4 (:side square)))
 
(​defmethod​ area :square [square]
 
(​*​ (:side square) (:side square)))

Let’s add a square to our vector of test shapes:

ClojureExamples/src/mbfpp/oo/visitor/examples.clj
 
(​def​ more-shapes (​conj​ some-shapes
 
{:shape-name :square :side 4}))

And we can verify that our operations work on squares as well:

 
=> (for [shape more-shapes] (perimeter shape))
 
(25.132741228718345 8 16)
 
=> (for [shape more-shapes] (area shape))
 
(50.26548245743669 4 16)

We’ve only scratched the surface of what multimethods can do. Since we can specify an arbitrary dispatch function, we can dispatch on just about anything. Clojure also provides a way to make multimethods work with user-defined hierarchies, much like class hierarchies in object-oriented languages. However, even the simple usage of multimethods we just saw is enough to replace the interesting aspects of the Visitor pattern.

Discussion

Scala has a much harder problem to solve here, since it maintains static type safety while allowing for extensions both to implementations of a data type and the operations performed on it. Since Clojure is dynamically typed, it has no such requirement.

Our Visitor replacements are a great example of the tradeoffs between an expressive statically typed language like Scala and a dynamically typed language like Clojure. We had to expend more effort in Scala, and our solutions aren’t quite as straightforward as the Clojure solutions. However, if we try to perform some operation on a type that can’t handle it in Clojure, it’s a runtime rather than a compile-time problem.

For Further Reading

Design Patterns: Elements of Reusable Object-Oriented Software [GHJV95]Visitor

Related Patterns

Pattern 9, Replacing Decorator

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.222.93.132