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
STRING
s into an int
. That's happening in
add(Tree,Tree)
. That's where you're assuming that you
have INTEGER
s. 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
INTEGER
s. 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
STRING
s. 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.
- Define an interface
OperatorAlgorithm
. - Define a class
AdditionAlgorithm
. - Define a method
add(ExpressionTIR,ExpressionTIR)
inOperatorAlgorithm
and inAdditionAlgorithm
. - 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.
- Define a new
add(ExpressionTIR,ExpressionTIR)
which throws an illegal-state exception. - 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.