| |
Harvey Mudd College
Computer Science 60
Assignment 5, Due Wednesday, February 24, by midnight
Unicalc and Unilang
This homework asks you to use Java to implement the Unicalc application that you completed
in Scheme in weeks 2 and 3. You will use the OpenList data structure
as teh fundamentak building block -- OpenList, after all, is designed to implement
Scheme-like list structures. Because Java is organized around creating
and combining data structures, we will Java-ize Unicalc by writing a Quantity class
that represents a quantity list.
This port of unicalc to Java is the first half of the assignment.
In the second part of the assignment, you'll use this unicalc-in-Java in order to
implement a language based on the unit-conversion
capability, similar in spirit to the Scheme CLI.
Implementing the language will require you to have a Tokenizer class (you have the option to use the one
we've provided, if you wish),
to write a Parser class, and to write an Evaluator class. Examples and, optionally, starting points
for the Parser and Evaluator are provided via links at the top-level assignment page.
Why so much code provided? I'd prefer to write the application myself... .
Please do! With the support code we are balancing two valid concerns: on one hand, the deep understanding that a from-scratch implementation provides and, on the other hand, enough examples and framework to build familiarity with what are certainly intricate - and important! - computational ideas. Feel free to use as much - or as little - of the support code as you wish. One important thing we do ask if you implement from scratch is to use (or replicate) our toString conventions, so that we can check your results efficiently!
Submitting your code
You will want to submit a zip file named hw5.zip
of your folder containing your code -- be sure to include at least these files:
- OpenList.java (which you might make changes to), and
- Tokenizer.java (you might make changes), and
- Quantity.java (Problem 1) and
- Parser.java (Problem 2) and
- Evaluator.java (Problem 3)
Submit this zip file in the usual way, under homework #5.
Both Windows and Mac OS X
have a mouse-menu option to archive a folder (control-click with a Mac; right-click with
a Windows machine).
Keep everything in one directory...
Because the .java files you write this week will be
interdependent, keep them all in a single folder/directory. That way,
java will be able to find classes as it needs to.
If you are using Dr. Java, keeping everything in one directory
should work out-of-the-box.
Part 0: Changes to OpenList
0 points
While it's not required, I found it very helpful to add the nonstatic methods
public Object second() ...
public Object third() ...
to my OpenList class, because the second and third elements
of lists get accessed lots of time in what follows!
As promised, with this assignment and beyond there is no need to have both the static
and nonstatic versions of these methods. Unless you want to... . Do feel free to add any other helper methods to your
OpenList class - just be sure to submit your OpenList.java file!
If you use the OpenList class provided in the hw5.zip archive, these methods
are already present.
Part 1: Unicalc in Java via Quantity
40 points
This problem asks you to implement the Unicalc application in Java by building
a class named Quantity. A starting Quantity.java file
is available the top-level assignments page in hw5.zip. You don't have to start with this
file, if you'd like to implement from scratch (please be sure your toString method
for Quantity objects matches ours, however!) On the other hand, you're more
than welcome to use the provided Quantity.java as a starting point, if you wish... .
The Quantity class
Objects of type Quantity will represent quantity lists, that is, something of the form
{ 9.8, [meter], [second, second] }
Note that a Quantity is delimited by curly braces. It will always contain three elements.
An object of type Quantity can also hold only a value - and it will always be a double
value:
{ 42.0, [], [] }
Here, we are illustrating the provided toString function, along with the OpenList
class you wrote last week. The curly braces are used to help distinguish objects of type Quantity
from objects of type OpenList. We encourage you to use your own OpenList class again
for this assignment -- and to augment it with any functions that may be helpful! If you'd prefer,
you may also start with our OpenList solution, which is part of the hw5.zip starter files.
Here is an overview of what capabilities Quantity.java should have
- Data members:
private double m; // the multiplier
private OpenList N; // the numerator, an OpenList of Strings (units)
private OpenList D; // the denominator, also an OpenList of Strings (units)
- Methods:
public Quantity(double m, OpenList N, OpenList D) ... // constructor
public toString() ... // PROVIDED prints objects of type Quantity
public static OpenList getDB() ... // PROVIDED gets a pre-prepared database of units
// the getDB() method is at the very bottom of the provided Quantity.java file
public double getm(); // "getter" - returns the multiplier
public OpenList getN(); // "getter" - returns the numerator
public OpenList getD(); // "getter" - returns the denominator
// These six are translations from prior weeks' Unicalc code
// you may replace these with others, as long as functionality is preserved
public static Quantity multiply(Quantity Q1, Quantity Q2); // multiplies the inputs
public static Quantity divide(Quantity Q1, Quantity Q2); // divides the inputs
public static Quantity norm_unit(String unit, OpenList DB); // normalizes unit in DB
public static Quantity norm(Quantity Q, OpenList DB); // normalizes Q in DB
// These next two are new for this week... ! See the notes below.
public static Quantity negate(Quantity Q); // returns negative of Q
public static Quantity plus(Quantity Q1, Quantity Q2, OpenList DB); // norm(Q1,DB) + norm(Q2,DB)
- and main:
public static void main(String[] args); // for testing your code!
Helper functions You're welcome to write helper functions to
implement the Unicalc API in Java. Two signatures that reflect the Unicalc solutions
in the provided Unicalc.scm file are these:
public static Quantity simplify(Quantity Q); // simplifies the input, Q
public static Quantity conv_unit(String unit, OpenList DB); // looks up unit in DB
but using others (or no helper functions) is OK, too.
Presuming the numerators and denominators are sorted We will
test your code only with Quantity lists whose numerators and denominators have units
in sorted order. You're welcome to include a call to sort in the
constructor of a Quantity, just to be sure! A sort routine is
provided in the provided OpenList class - or you might use your own... .
How do I compare two Strings in Java? As in the
merge method that was part of last week's OpenList
class, you will want to use the method compareTo
String s1 = "apple";
String s2 = "zebra";
if (s1.compareTo(s2) < 0) ... // this will be true!
compareTo returns a negative value if this (the calling
object) is earlier in the dictionary (i.e., ASCII order) than the input argument
s2. If they're equal 0 is returned. If s2 is
earlier, a positive number is returned.
The compareTo method is fully documented at
this link. There is also a compareToIgnoreCase, but to be
consistent with our tests, please use compareTo here.
Like OpenLists, objects of type Quantity are not
meant to be changed. Rather, the methods above return new objects of
type Quantity based on their inputs. Indeed, six of the methods
above are based on the prior Unicalc assignment, a solution to which
is posted at the top-level assignment page, if you'd like to use it. The reason
that these six are all static is to more closely resemble their
Scheme counterparts.
Two new methods
There are two new methods that were not part of the Scheme unicalc application:
-
public static Quantity negate(Quantity Q) This method should return
a new object of type Quantity with the same numerator and denominator as Q,
but with a multiplier of opposite sign.
-
public static Quantity plus(Quantity Q1, Quantity Q2, OpenList DB) This method should return
a new object of type Quantity that represents the sum of Q1 and Q2
after normalizing each one in the database DB. If, after normalizing both inputs,
their units are identical, addition is straightforward. On the other hand, if the units are not
identical, a warning message should be printed out and an error list, such as { 0.0 [] [] } or
{ 0.0 ["addition error"] [] },
should be returned. This will allow the unicalc-language interpreter to avoid crashing while
still alerting the user to an error. Indeed, the former example will be very forgiving - perhaps too
forgiving - of user errors! However, we will only test your language with valid inputs.
Note to experienced (and new) Java-ers: creating an "error" version of a Quantity object is not
really the "Java" way of handling such a problem. Rather, we culd use Java's exception mechanism by
throwing an exception (error) and catching it elsewhere. But there's enough in this assignment already!
Testing your code
You should write your main method so that it constructs several objects
of class Quantity and tests all of your class's methods. There are a few
tests (commented out) that are already in the provided main. If you use
your own OpenList class, you might not be able to run
all of the tests provided -- that's OK, just comment the others out.
We will also run some additional tests of our own.
We will only test your code on valid, commensurate inputs, for example, for the plus method.
Part 1.5: Tokenizer
A Tokenizer.java file provided for you in the code at the top-level assignment page.
You may want to improve it or create your own tokenizer, but
neither of these is required. See the optional extra credit portion of the assignment for additional
ideas... .
Part 2: Parsing the Unicalc language via Parser
30 points
This problem asks you to implement a Parser for the Unicalc language, whose grammar is as follows:
Technical note on the grammar
The rule for Q uses the asterisk, not as "times," but meaning "zero or more of the
preceding symbol." It also uses the plus sign, not as addition, but meaning "one or more of the
preceding symbol." Both of these are implemented in the provided Q method in
Parser.java.
The Parser class
To do this, you should write a Parser class in a file named Parser.java. This
should implement a recursive-descent parser for the above grammar, yielding an OpenList
that represents the parse tree for a provided set of valid tokens.
There is a starting point for such a class available in the startingFiles.zip archive
at the top-level assignment page.
In particular, that provided code already implements a number of methods for checking if there
are remaining tokens (and what they are). In addition, it provides an implementation of
Q(), for the Q nonterminal symbol.
Overview of parsing
The method that will begin all the work - even though all of that work will be
delegated recursively - is parse:
public OpenList parse(String[] tokens) This method should return
a new object of type OpenList that represents the parse tree for that set of tokens.
We will not test your code on invalid sets of tokens. Here is a simple example of each one - keep in mind
that they can be composed, as well. Also, double quotes have been added to highlight strings, but
they will not be printed by Java.
USER INPUT PARSE TREE
---------- ----------
def x 42 ["def", "x", [{ 42.0, [], [] }]]
# 1 joule ["#", [{ 1.0, ["joule"], [] }]]
1 foot + 1 inch ["+", [{ 1.0, ["foot"], [] }], [{ 1.0, ["inch"], [] }]]
1 foot - 1 inch ["-", [{ 1.0, ["foot"], [] }], [{ 1.0, ["inch"], [] }]]
1 second * 2 hz ["*", [{ 1.0, ["second"], [] }], [{ 2.0, ["hz"], [] }]]
1 second / 2 hz ["/", [{ 1.0, ["second"], [] }], [{ 2.0, ["hz"], [] }]]
(1 cm) [{ 1.0, ["cm"], [] }] // note - no extra nesting!
(- 1 cm) ["-", [{ 1.0, ["cm"], [] }]]
42.42 meter [{ 42.42, ["meter"], [] }]
meter [{ 1.0, ["meter"], [] }]
42 [{ 42.0, [], [] }]
2+3*4 [+, [{ 2.0, [], [] }], [*, [{ 3.0, [], [] }], [{ 4.0, [], [] }]]]
and here is a concise summary of the resulting parse tree from each expression in the grammar:
Other methods for Parser
Recursive-descent parsing uses a method for each nonterminal in the grammar - except for D
and V in the case of this problem. One way to build up to the full grammar is to implement
one nonterminal method at a time, for example, from the bottom towards the top. So, you would implement
and test U(), and then P(), and then E(), and finally S().
Testing your code
The main method is already written to make this easy - you should be
able to try out all of the above inputs (and many more of your own) to be sure
that your Parser is working correctly.
Part 3: Evaluating the Unicalc language via Evaluator
30 points
This part completes the unicalc language. Here, you should build an Evaluator class
that has at least the following four things:
- Data members:
private OpenList env; // the environment - a unit-database association list!
- Methods:
public Evaluator(OpenList env) ... // constructor
public Quantity evaluate(OpenList parseTree) ... // returns the result, a Quantity
public static void main(String[] args) ... // for testing
There is a starting file for Evaluator.java, which evaluates Quantitys;
it is available from the startingFiles.zip archive at the top-level
assignment page. Again, its use is 100% optional.
What does everything evaluate to? Well, every valid statement
in the Unicalc language evaluates to an object of type Quantity, that is, a
quantity list.
For example, the parse tree which is simply a list of a Quantity
naturally evaluates to that Quantity object.
Many of the others are clear: addition, subtraction, negation, multiplication,
division, and normalizing. Some of these operations (addition and normalizing) require a
database of units... this database is the environment in which each expression
is evaluated. The data member env (already at the top of
Evaluator.java is provided for this purpose.
This environment env starts out empty -- meaning that every unit
is a basic, irreduceable unit. However, as you define units with def,
conversions become possible. These are considered next.
The two parse trees whose evaluations may not be clear are these:
-
[ "#" E ] This parse tree should evaluate to the normalized equalivalent of
E within the current environment. # is simply the operator for normalizing.
-
[ "def" V E ] This parse tree should evaluate to the value of
E. After all, because of Java's strong typing, it will complain if your
code does not return an object of type Quantity, and the value of E
will be of exactly that type. However, the much more important function of def
is that
it should add an association to the front of the environment's association list! This is probably best explained by example. Suppose that the Evaluator's association list env is at first
[
["mile", { 5280.0 ["foot"] [] }]
]
with the quotes and spacing added only for emphasis.
Then, after the statement def league 0.125 mile, the environment
should become
[
["league", { 0.125, ["mile"], [] }],
["mile", { 5280.0 ["foot"] [] }]
]
We could even redefine "mile" - for example, after def mile 1760 yard, the new value of
env would be
[
["mile", { 1760.0, ["yard"], [] }],
["league", { 0.125, ["mile"], [] }],
["mile", { 5280.0 ["foot"] [] }]
]
and assoc should "do the right thing" by taking the first match at the top.
The data member env, already provided in the Evaluator, is the natural
place to store the environment... .
Testing your code
The main method is already written to make debugging your
language easy. If you downloaded the starter files recently,
you will likely see like similar to the following in main
of Evaluator.java file:
boolean PRINT_ALL = true;
// the read-eval-print while loop is here...
if (PRINT_ALL)
System.out.println("The tokens are " + Tokenizer.printArray(tokens));
On the other hand, if you downloaded the code earlier, you might see
lines that test the number of command-line arguments, such as
if (args.length > 0)
System.out.println("The tokens are " + Tokenizer.printArray(tokens));
In this case, feel free to change the tests if (args.length > 0)
to the always-true versions as follows:
if (args.length >= 0)
As a result, the tokens, parse tree, evaluation, and environment will always
print out.
When the graders run your file, they will make sure to trigger the
printing of all of
this debugging information, because it makes it easier to understand
the behavior of the code!
If you'd like a copy of this new main to drop in your code,
a version is available
for copy-and-paste from this link.
Things to check:
Here are some examples, including
the printout of the tokens, parse tree, value, and environment each time:
def mile 5280 foot
The tokens are < def, mile, 5280, foot >
The parseTree is [def, mile, [{ 5280.0, [foot], [] }]]
The value is { 5280.0, [foot], [] }
The current env is
[[mile, { 5280.0, [foot], [] }]]
def yard 3 foot
The tokens are < def, yard, 3, foot >
The parseTree is [def, yard, [{ 3.0, [foot], [] }]]
The value is { 3.0, [foot], [] }
The current env is
[[yard, { 3.0, [foot], [] }], [mile, { 5280.0, [foot], [] }]]
1 mile + 2 yard
The tokens are < 1, mile, +, 2, yard >
The parseTree is [+, [{ 1.0, [mile], [] }], [{ 2.0, [yard], [] }]]
The value is { 5286.0, [foot], [] }
The current env is
[[yard, { 3.0, [foot], [] }], [mile, { 5280.0, [foot], [] }]]
def x 2 mile - 1 foot
The tokens are < def, x, 2, mile, -, 1, foot >
The parseTree is [def, x, [-, [{ 2.0, [mile], [] }], [{ 1.0, [foot], [] }]]]
The value is { 10559.0, [foot], [] }
The current env is
[[x, { 10559.0, [foot], [] }], [yard, { 3.0, [foot], [] }], [mile, { 5280.0, [foot], [] }]]
x / 1 foot
The tokens are < x, /, 1, foot >
The parseTree is [/, [{ 1.0, [x], [] }], [{ 1.0, [foot], [] }]]
The value is { 1.0, [x], [foot] }
The current env is
[[x, { 10559.0, [foot], [] }], [yard, { 3.0, [foot], [] }], [mile, { 5280.0, [foot], [] }]]
# x / 1 foot
The tokens are < #, x, /, 1, foot >
The parseTree is [#, [/, [{ 1.0, [x], [] }], [{ 1.0, [foot], [] }]]]
The value is { 10559.0, [], [] }
The current env is
[[x, { 10559.0, [foot], [] }], [yard, { 3.0, [foot], [] }], [mile, { 5280.0, [foot], [] }]]
3 yard yard + 1 foot * 2 foot
The tokens are < 3, yard, yard, +, 1, foot, *, 2, foot >
The parseTree is [+, [{ 3.0, [yard, yard], [] }], [*, [{ 1.0, [foot], [] }], [{ 2.0, [foot], [] }]]]
The value is { 29.0, [foot, foot], [] }
The current env is
[[x, { 10559.0, [foot], [] }], [yard, { 3.0, [foot], [] }], [mile, { 5280.0, [foot], [] }]]
Optional extra credit: functionalization!
Functions! For up to an additional +25 points (and this won't return
next week...), provide an implementation of functions in the
Parser and Evaluator
of the unicalc language. How to design and implement this is 100% up to you. In the
spirit of the add-your-own-feature option (below) however, please be sure
- not to change or break valid statements in the language as specified in the assignment
(so we can still grade it!)
- to describe how you had to change your Parser and Evaluator to make this work...
- to document
what you did and how we can test it, including how you handle input parameters
and unbound symbols in the body of a function.
Put your explanation in a comment at the top of Evaluator.java.
Give the graders a couple of examples to try!
Here is a (very small) image of one possible grammar for introducing functions into the language.
It's not the only approach, to be sure. Click on the image for a larger version.

Personalizing For up to +5 points, improve the unicalc-language with whatever
feature you'd like... . Here are the rules, however:
- do not change or break valid statements in the language as specified in the assignment (so we can still grade it!)
- in a comment at the top of Evaluator.java document what you did and how we can test it.
Give the graders a couple of examples to try.
|