Lab 9: The Visitor Pattern
Overview
In this lab, we will learn about the visitor pattern. The visitor pattern is a way of organizing software so that it can be more extensible.
With the benefit of extensibility comes some increase in complexity: the visitor pattern uses a lot of indirection to do its work. We often must resort to this complexity when the programming language we want to use does not have the features we wish it did. In our case, Java does not have pattern-matching like Haskell does. So, we will see how to use the visitor pattern to simulate Haskell’s pattern-matching function definitions in Java.
The goals of this lab are:
- Appreciate the benefits of pattern-matching.
- Introduce the visitor pattern as a way to simulate pattern matching.
- Practice using the visitor pattern to add new functionality to existing code, without having to modify the existing code.
In this week’s assignment, we will write a code-generator that outputs visitor-pattern-enabled Java code.
Materials
Use the assignment workflow and the link provided on Piazza to get access to this week’s files.
Background: Differences between Haskell and Java
To motivate the Visitor Pattern, we first need to highlight some differences between Haskell and Java. To do so, we will use a running example.
The example: Animals
Let us pretend that we want to write some code that represents animals, for instance, to be used in an animation for a movie or video game. In particular, we want to represent the following animals:
- A cow, which can have a name and which has a specified number of legs.
- A snake, which can have a species and a specified level of “slitheriness”.
- A fish, which can live in saltwater (or not).
Additionally, we want to provide the ability to ride an animal.
Let us see how we would represent these ideas in Haskell and in Java.
Implementing animals in Haskell
- To implement the different kinds of animals (such as a cow), we use data types, with a tag for each specific kind of animal and associated values for the properties of an animal.
- To implement an operation (such as ride), we use a function, defined using pattern-matching on the data type.
- To ride a cow, we call the
ride
function, passing a cow.
Your task: In the provided code, the implementation is in the
animals/haskell/animal.hs
file. Look it over now, to check that you understand what it is
doing. There is also a video walkthrough of the code, in
case anything is unfamiliar to you.
Implementing animals in Java
- To implement the different kinds of animals (such as a cow), we use a class hierarchy, with an abstract class for a generic animal and an inherited, concrete class for each specific kind of animal. Each concrete class has fields to represent the properties of an animal.
- To implement an operation (such as ride), we use methods and dynamic dispatch. The method is declared in the abstract class and overridden / implemented in each concrete class.
- To ride a cow, we call the
ride
method of aCow
object.
Your task: In the provided code, the implementation is in the
animals/java/animal-basic
directory. Look it over now, to check that you understand what
it is doing There is also a video walkthrough of the code,
in case anything is unfamiliar to you (or if it has been awhile since you last saw Java!).
Adding a new operation
Now, let us say that we want to add an eat
operation for animals. How would we do that
in Haskell and in Java?
Adding a new operation in Haskell
To add a new operation in Haskell, we can write one new function, eat
, with a case for each
existing animal.
Your task: In the provided code, the implementation is in the
animals/haskell/animal-eat.hs
file. Look it over now and / or watch the video
walkthrough of the code.
Adding a new operation in Java
To add a new operation in Java, we have to modify all the classes we already
defined, to add the new eat
method to each class.
Your task: In the provided code, the implementation is in the
animals/java/animal-eat
directory. Look it over now and / or watch the video
walkthrough of the code.
Although it seems fairly straightforward to add a new operation to the Java class hierarchy, notice one potential problem: We must modify the existing class definitions. This is a problem because it is not modular. What if the original animal code was given to us as a library, and we did not have access to the source code to be able to modify it? We would be stuck.
Is it possible to add a new operation to a Java class hierarchy, without having to modify any of the existing code? Yes, there are several ways to do so. We will focus on one way, called the Visitor Pattern.
Background: The Visitor Pattern
The Visitor Pattern gives us a way to simulate pattern-matching in a language (such as Java) that does not support pattern-matching. We can take advantage of this simulation to add new operations to an existing class hierarchy.
Your task: Watch this video (password
VisitorIsCase
) that motivates, develops, and explains the visitor pattern. The video
starts with a recap of the original Haskell and Java for riding animals and introduces new
code. The code provided with the lab helps you follow along with the video.
To recap, here is how we would represent animals using the visitor pattern:
- To implement the different kinds of animals (such as a cow), we use a class hierarchy, with an
abstract class for a generic animal and an inherited, concrete class for each specific
kind of animal. Each concrete class has fields to represent the properties of an animal.
- Each class also has an
accept
method, which will be used to call operations on that class.
- Each class also has an
- We simulate a case statement (or pattern-matching) using a special “visitor” interface
for animals.
- The interface declares a
visit
method for each kind of animal.
- The interface declares a
- To implement an operation (such as
ride
), we write a class that conforms to (i.e., implements) the visitor interface. Let us call the classRideVisitor
.- The
RideVisitor
class implements all thevisit
methods, to define howride
works on each kind of animal.
- The
- To ride a cow, we need an object of the
RideVisitor
class, which we pass to aCow
object’saccept
method, which in turn calls theRideVisitor
’svisitCow
method, which defines how to ride cow. TheRideVisitor
object stores the result of theride
operation, which we can then access. (Whew!)- In other words, where we used to say this:
String result = cow.ride();
we now say this:
RideVisitor visitor = new RideVisitor(); cow.accept(visitor); String result = visitor._result;
- In other words, where we used to say this:
The visitor pattern can be difficult to understand at first, because it uses so much indirection. But it is also powerful and shows up in quite a few real-world applications.
To start to gain more comfort with the visitor pattern, we will implement a new visitor for animals.
Practice: Write a visitor
In this part of the lab, we will revisit (no pun intended!) our stack machine from earlier in the semester.
Recall: Stack machine
Recall that our first Haskell assignment asked us to implement arithmetic operations using a stack machine.
We represented the operations using a list of “stack instructions” (such as push and swap), which operate on a stack. The result of the arithmetic operation is the value on top of the stack after all the operations have been evaluated.
Your task: The file stack/haskell/stack.hs
contains our AST for stack
instructions. Look over this file to remind yourself of the stack operations.
Read and understand a Java visitor-pattern implementation
Your task: The directory stack/java/stack
contains a Java translation of our
stack-instruction AST, with support for the visitor pattern. The file
stack/java/StackShowVisitor.java
contains an implementation of a “show” operation for
stack instructions. The file stack/java/ExampleStackProgram.java
contains a program that
uses the visitor. Look over these files and make sure you understand how they work
together.
You can run ExampleStackProgram.java
by clicking Run
in VS code (if you have the Java
Extension Pack) installed. You can also run these commands in the terminal, from the
stack/java
directory:
javac *.java stack/*.java
java ExampleStackProgram
Implement a new visitor for stacks
The file stack/java/Has42Visitor.java
contains an incomplete implementation of a
visitor. The intent of the visitor is to determine whether an instruction mentions the
literal value 42.0
.
Your task:
- Complete the implementation of
Has42Visitor
. - In
stack/java/ExampleProgram.java.
, use your new visitor to count the number of instructions in the arrayinstructions
that mention the literal value42.0
(there should be only one). Print the result to the screen.
Note: It may be helpful to first think, “How would I write this in Haskell?”. The Java code is both easier than it seems (in the sense that there is not much to write) and more complicated (because we are using the visitor pattern).
You can run your updated ExampleStackProgram.java
by clicking Run
in VS code (if you
have the Java Extension Pack) installed. You can also run these commands in the terminal,
from the stack/java
directory:
javac *.java stack/*.java
java ExampleStackProgram