CS 214: Programming Languages
Spring 2009

Home|Syllabus|Schedule
<<|>>|ANTLR API|CITkit API|PolyD API|EasyMock API

Adding strings and integers
Interpreters, Iteration 7

User Story

Back to this story, with a clarification:

User Story #5: Interpreter evaluates a two-value sum.

A Hobbes program can be the addition of two values (any combination of integers and strings).

Examples:

code evaluation
2 + 3 5
"hi " + "there" "hi there"
"abc" + 123 "abc123"
456 + "sticks" "456sticks"

In order for this to work, you have to now be sensitive to the type of data that you're working with. Previously, you've assumed that the data in an addition was integer. Now you have to check.

Procedural Solution

If you deal with other operators (like multiplication), you'll probably want to first decide which operator you have and then deal with the operands. You want to shoot for code like this in interpret(Tree):

if (tree.getText().equals("+")) {
  return add(tree.getChild(0), tree.getChild(1));
} else if (tree.getText().equals("*")) {
  return multiply(tree.getChild(0), tree.getChild(1));
} else {
  throw new IllegalArgumentException(tree.getText() + " is unrecognized");
}

Reminder: "refactor" means change the code without breaking any tests.

Hint: add(Tree,Tree) and multiply(Tree,Tree) would be private instance methods.

Questions to ponder: Why a multibranch if? Why not a switch? Why throw the exception?

Refactor your code into this form. Use Extract Method, Extract Local Variable, Inline Method, Inline Local Variable, Rename, etc. (Don't just copy and paste what you see above.) Compile, and green bar.

This reduces your problem to the add(Tree,Tree) method. Get the tests complaining about this first.

Create a new test method shouldInterpretStringAddition() in ProceduralHobbesTest. Add an assertion for adding two strings together. Compile, and red bar.

The red bar should be a complaint about turning one of your STRINGs into an int. That's happening in add(Tree,Tree). That's where you're assuming that you have INTEGERs. Your code shouldn't make this assumption any more.

Use the getType() method and HobbesParser.INTEGER with an if to ensure that the existing code runs only when the two parameters are INTEGERs. Create a default case that throws an IllegalArgumentException which complains that the parameters aren't of the right type. Compile, and red bar.

You haven't fixed the STRING issue, but you should get a different cause for the red bar: the default-case exception. The INTEGER case should still work just fine.

Question to ponder: why a multibranch if? (Hint: the answer is different this time!)

Add the multibranch if in add(Tree,Tree) to check if the two arguments are STRINGs. If so, return the string concatenation. Compile, and green bar.

Write two more assertions for STRING plus STRING. At least one of them should use an empty string. Compile, and green bar.

Write a new test method shouldInterpretStringPlusInteger(), write assertions for this test method, compile, red bar, fix add(Tree,Tree), compile, green bar.

Write a new test method shouldInterpretIntegerPlusString(), write assertions for this test method, compile, red bar, fix add(Tree,Tree), compile, green bar.

Write some sample input files with these new types of additions, and try your driver on them.

Object-Oriented Solution

New JAR Files

Don't get clever and try for a newer ASM version. It won't work!

Download these files:

Put them in your lib folder, and add them to your build path.

Better Object-Oriented Dispatching

Remember those (IntegerETIR) casts you have in visitOperatorETIR(IOperatorETIR)? They're evil. They're not object oriented.

The problem is that standard OO solutions allow one object to determine which algorithm (i.e., method) to execute. Suppose I had these methods in IntegerETIR for you:

public IntegerETIR add(IntegerETIR) { ... }
public StringETIR add(StringETIR) { ... }

You might be tempted to write this code for visitOperatorETIR(IOperatorETIR):

return operator.getLeft().add(operator.getRight());

This glosses over the return type of getLeft() which is also a problem!

Here's the kicker: getRight() officially returns an ExpressionTIR. In the scenario above, there's no add(ExpressionTIR), and so the compiler will complain! You can yell and rant at the compiler: "I know it'll be an IntegerETIR or a StringETIR!" But it won't care.

You could check to see what kind of data it is and then cast it, but checking data types and casting is not object oriented.

This is often known as the binary operator problem. You have a binary operator (addition in this case), and you want to pick which method to execute at runtime based on two objects. Most OO languages (C++, Java, Ruby, Python, etc.) allow you to pick a method at runtime based on only one object—standard polymorphism. To pick a method based on two or more objects, you need multiple dispatch.

The programming language Dylan uses multiple dispatch all of the time. It appears that Perl 6 will have it. Some languages (like Python) allow you to add it. Some have it implemented in a library. You have this last option in Java.

The PolyD library provides multiple dispatch in Java. But instead of working on our data objects, you have to implement an algorithm object. You're already doing something like this with the ExpressionTIRVisitor interface. Your OOHobbesInterpreter is really an algorithm encapsulated in an object. Now you need another algorithm object to encapsulate string-integer addition.

Before you add new features, let's refactor your code into an addition algorithm.

You need to be able to change the addition code without having to change the multiplication code.

Again, refactor your code so that addition and multiplication (and any other arithmetic operators) are handled separately.

You want to shoot for something like this to handle addition in visitOperatorETIR(IOperatorETIR):

OperatorAlgorithm addition = PolyD.build(OperatorAlgorithm.class, new AdditionAlgorithm());
return addition.add(operator.getLeft(), operator.getRight());

Add this code to visitOperatorETIR(IOperatorETIR); comment out the old addition code.

Don't forget about Eclipse's Quick Fix (ctrl-1). It'll define missing classes and interfaces and methods.

You'll get compiler errors about missing classes and methods.

  1. Define an interface OperatorAlgorithm.
  2. Define a class AdditionAlgorithm.
  3. Define a method add(ExpressionTIR,ExpressionTIR) in OperatorAlgorithm and in AdditionAlgorithm.
  4. Finally, tell PolyD what to do with the OperatorAlgorithm interface (see below).

PolyD needs to know how to dispatch methods based on the arguments passed to a method. You're interested in multiple dispatch, so it'll look like this:

@PolyD
@DispatchingPolicy(MultiDisp.class)
interface OperatorAlgorithm { ... }

So when you have PolyD build an algorithm with this interface (the OperatorAlgorithm.class from earlier), it'll use multiple dispatch (the MultiDisp.class here). And it will dispatch using the methods that you define in AdditionAlgorithm (the new AdditionAlgorithm() from earlier).

So now you just have to add methods to AdditionAlgorithm with the parameters you're really interested in. For example, you could stick with the evil casting code:

Move the old addition code from visitOperatorETIR(IOperatorETIR) into add(ExpressionTIR,ExpressionTIR). Compile, and green bar.

But, of course, one purpose of using PolyD is so that you don't have to do the casting.

  1. Define a new add(ExpressionTIR,ExpressionTIR) which throws an illegal-state exception.
  2. Change the old version into add(IntegerETIR,IntegerETIR); drop the unnecessary casts and local variables, and just compute the addition with the parameters!

Compile, and green bar.

The real power, though, with multiple dispatch is that AdditionAlgorithm and PolyD will figure out which add() method should be called no matter what values are passed to it.

Add a shouldInterpretStringAddition() test method to OOHobbesInterpreterTest with one assertion for string addition. Compile, and red bar.

The red bar should be due to the exception in add(ExpressionTIR,ExpressionTIR).

Write an add(StringETIR,StringETIR) in AdditionAlgorithm. Hard code its return value so that it'll pass its assertion. Compile, and green bar.

Write two more assertions for string addition, compile, and red bar. Fix add(StringETIR,StringETIR), compile, and green bar.

The two parameters in an add() method don't have to be the same.

Follow the same process as above to implement add(StringETIR,IntegerETIR).

Follow the same process as above to implement add(IntegerETIR,StringETIR).

Run both drivers on your various addition programs.