CS2030 Practical Assessment #2

Back to homepage

When teaching Java programming to beginners, static variables (or class variables) are introduced as a way for the class to keep track of aggregated data (e.g. keeping a count of the number of objects created). The example below illustrates the Circle class which uses a mutable numOfCircles class variable.

class Circle {
    private static int numOfCircles = 0;
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
        numOfCircles = numOfCircles + 1;
    }

    double getRadius() {
        return this.radius;
    }

    static int getNumOfCircles() {
        return Circle.numOfCircles;
    }

    @Override
    public String toString() {
        return "Circle of radius " + this.radius;
    }
}

jshell> new Circle(1).getNumOfCircles()
$.. ==> 1

jshell> new Circle(1).getNumOfCircles() // same expression; different result
$.. ==> 2

It should be noted that the class variable is mutable, and every creation of a Circle object would have a side-effect in that the external state numOfCircles has to be updated.

Task

We shall look at creating an "aggregation context" to wrap around an object while maintaining immutability with no side-effects. There are altogether four levels. You need to complete ALL levels.

Take Note!

As this exercise involves a very specific application with few classes, your code will not be checked for cyclic dependencies. Moreover, there are NO restrictions on the Java control flow constructs you can use :)

Level 1

Define a generic Count class that wraps around an object of type T.

jshell> Count.<Circle>of(new Circle(1.0))
$.. ==> (1, Circle of radius 1.0)

jshell> Count.<Circle>of(new Circle(1.0)).
   ...> map(new Circle(2.0))
$.. ==> (2, Circle of radius 2.0)

jshell> Count<String> one = Count.<String>of("one")
one ==> (1, one)

jshell> one.map("two").map("three")
$.. ==> (3, three)

jshell> one
one ==> (1, one)

Level 2

Counting the number of objects created is just one of the many ways aggregation can be done. As such, we would like the client to specify the aggregation function to be performed.

Define a class Aggregate<S,T> with the following methods

Moreover, since counting is a form of aggregation, make Count a sub-class of Aggregate. Note that Count should NOT have any properties now.

jshell> Aggregate.<Integer,Circle>seed(0)
$.. ==> (0)

jshell> Aggregate.<Integer,Circle>seed(10)
$.. ==> (10)

jshell> Aggregate.<Integer,Circle>seed(1).
   ...> map(x -> x * 2, new Circle(2.0))
$.. ==> (2, Circle of radius 2.0)

jshell> Count<String> count = Count.<String>of("one").
   ...> map("two").map("three")
count ==> (3, three)

jshell> Aggregate<Integer,String> agg = count.map("four")
agg ==> (4, four)

jshell> count
count ==> (3, three)

jshell> Aggregate.<Integer,Circle>seed(0).
   ...> map(x -> x + 1, new Circle(1.0)).
   ...> map(x -> x + 2, new Circle(2.0))
$.. ==> (3, Circle of radius 2.0)

jshell> Aggregate.<Double,Circle>seed(0.0).
   ...> map(x -> x + 1.0, new Circle(1.0)).
   ...> map(x -> x + 2.0, new Circle(2.0))
$.. ==> (3.0, Circle of radius 2.0)

Level 3

You will realize that the map method, such as map(x -> x + 2.0, new Circle(2.0)), performs aggregation independently from the object in the second argument. This would be an issue for certain updates such as "increment with the radius of the new Circle object" as it would be difficult to enforce consistency between the two arguments (unless we use an external variable).

jshell> Circle c = new Circle(2.0)
c ==> Circle of radius 2.0

jshell> Aggregate.<Double,Circle>seed(0.0).
   ...> map(x -> x + 1.0, new Circle(1.0)).
   ...> map(x -> x + c.getRadius(), c)
$.. ==> (3.0, Circle of radius 2.0)
An alternative would be to pass a Function (that outputs a Pair of values) to the map method instead. As an example,
jshell> Aggregate.<Double,Circle>seed(0.0).
   ...> map(x -> x + 1.0, new Circle(1.0)).
   ...> map(x -> {
   ...>    Circle c = new Circle(2.0);  // Circle c is now within the lambda 
   ...>    return Pair.<Double,Circle>of(x + c.getRadius(), c); 
   ...> })
$.. ==> (3.0, Circle of radius 2.0)

Define the Pair class with a static method of that constructs a pair of values, possibly of different types. Include this new mapping as an overloaded map method in the Aggregate class. There is no need to modify the Count class to make use of this new map method.

jshell> Pair.<Integer,String>of(1, "one")
$.. ==> (1, one)

jshell> Aggregate.<Double,Circle>seed(0.0).
   ...> map(x -> x + 1.0, new Circle(1.0)).
   ...> map(x -> Pair.<Double,Circle>of(x + 2.0, new Circle(2.0)))
$.. ==> (3.0, Circle of radius 2.0)

jshell> Aggregate.<Double,Circle>seed(0.0).
   ...> map(x -> x + 1.0, new Circle(1.0)).
   ...> map(x -> {
   ...>     Circle c = new Circle(2.0); 
   ...>     return Pair.<Double,Circle>of(x + c.getRadius(), c); 
   ...> })
$.. ==> (3.0, Circle of radius 2.0)

jshell> Count<String> count = Count.<String>of("one").
   ...> map("two").map("three")
count ==> (3, three)

jshell> Aggregate<Integer,String> agg = count.map("four").
   ...> map(x -> Pair.<Integer,String>of(x + 1, "five"))
agg ==> (5, five)

jshell> count
count ==> (3, three)

Level 4

Up till now, each map method has performed aggregation by updating the first value of the Pair. The second value is merely the most recent object involved in the aggregation. One wonders if there is any way to aggregate the objects at the same time, such as joining strings. Let's do this using flatMap!!

Include the flatMap method in Aggregate that takes in a Function<T,Aggregate<S,T>> constructed via the static of method. Below is an example:

jshell> Aggregate.<Integer,String>seed(0).
   ...> map(s -> Pair.of(s + 1, "one"))
$.. ==> (1, one)

jshell> Aggregate.<Integer,String>seed(0).
   ...> map(s -> Pair.of(s + 1, "one")).
   ...> flatMap(t -> Aggregate.<Integer,String>of(
   ...>    s -> Pair.<Integer,String>of(s + 2, t + " two")))
$.. ==> (3, one two)

Notice that we take in a function where the input is of the element type T, rather than the aggregate type. There is a very good reason for this as it allows us to specify the "mapping" for both the aggregation, as well as the element.

Aggregate.of cannot perform the actual aggregation by itself, i.e. without being part of a flatMap. Hence the output of the following is simply the string Aggregate.

jshell> Aggregate.<Integer,String>of(
   ...>    s -> Pair.<Integer,String>of(s + 2, "two"))
$.. ==> Aggregate

Chaining the of method after the seed method will result in an invalid aggregate.

jshell> Aggregate.<Integer,String>seed(11).
   ...> flatMap(t -> Aggregate.<Integer,String>of(
   ...>    s -> Pair.<Integer,String>of(s + 2, "two")))
$.. ==> Invalid Aggregate

jshell> Aggregate.<Integer,String>seed(11).
   ...> flatMap(t -> Aggregate.<Integer,String>of(
   ...>    s -> Pair.<Integer,String>of(s + 2, "two"))).
   ...> map(s -> Pair.of(s + 1, "one"))
$.. ==> Invalid Aggregate

Additionally, chaining map or flatMap methods after the of method will also render the aggregate invalid.

jshell> Aggregate.<Integer,String>of(
   ...>    s -> Pair.<Integer,String>of(s + 2, "two")).
   ...> map(s -> Pair.of(s + 1, "one"))
$.. ==> Invalid Aggregate

jshell> Aggregate.<Integer,String>of(
   ...>    s -> Pair.<Integer,String>of(s + 2, "two")).
   ...> map(s -> Pair.of(s + 1, "one")).
   ...> map(s -> Pair.of(s + 3, "three"))
$.. ==> Invalid Aggregate

jshell> Aggregate.<Integer,String>of(
   ...>    s -> Pair.<Integer,String>of(s + 2, "two")).
   ...> map(s -> Pair.of(s + 1, "one")).
   ...> flatMap(t -> Aggregate.<Integer,String>of(
   ...>    s -> Pair.<Integer,String>of(s + 2, t + " two")))
$.. ==> Invalid Aggregate

Study the following tests carefully and infer how Aggregate should be modified for this level.

jshell> Aggregate.<Integer,String>seed(0).
   ...> map(s -> Pair.of(s + 1, "one"))
$.. ==> (1, one)

jshell> Aggregate.<Integer,String>seed(0).
   ...> map(s -> Pair.of(s + 1, "one")).
   ...> flatMap(t -> Aggregate.<Integer,String>of(
   ...>    s -> Pair.<Integer,String>of(s + 2, t + " two")))
$.. ==> (3, one two)

jshell> Aggregate.<Integer,String>seed(0).
   ...> map(s -> Pair.<Integer,String>of(s + 1, "one")).
   ...> flatMap(t -> Aggregate.<Integer,String>of(
   ...>    s -> Pair.<Integer,String>of(s + 2, "two")))
$.. ==> (3, two)

jshell> Aggregate.<Integer,String>seed(0).
   ...> map(s -> Pair.<Integer,String>of(s + 1, "one")).
   ...> flatMap(t -> Aggregate.<Integer,String>of(
   ...>    s -> Pair.<Integer,String>of(s + 2, t + "two")))
$.. ==> (3, onetwo)

jshell> Function<Integer, Pair<Integer,String>> doit(String s) {
   ...>     return x -> Pair.<Integer,String>of(x + s.length(), s);
   ...> }
|  created method doit(String)

jshell> Function<String, Aggregate<Integer,String>> doit2(String s) {
   ...>     return x -> Aggregate.<Integer,String>of(y -> {
   ...>         Pair<Integer,String> pair = doit(s).apply(y);
   ...>         return Pair.<Integer,String>of(
   ...>            pair.first(), x + " " + pair.second());
   ...>     });
   ...> }
|  created method doit2(String)

jshell> Aggregate.<Integer,String>seed(0).
   ...> map(doit("one"));
$.. ==> (3, one)

jshell> Aggregate.<Integer,String>seed(0).
   ...> map(doit("one")).
   ...> flatMap(doit2("two")).
   ...> map(doit("three"))
$.. ==> (11, three)

jshell> Aggregate.<Integer,String>seed(0).
   ...> map(doit("one")).
   ...> flatMap(doit2("two")).
   ...> flatMap(doit2("three"))
$.. ==> (11, one two three)

jshell> Aggregate.<Integer,String>seed(10).
   ...> map(s -> Pair.of(s + 1, "one")).
   ...> flatMap(t -> Aggregate.<Integer,Integer>of(
   ...>    s -> Pair.<Integer,Integer>of(
   ...>       s + 2, t.length() + 5))).
   ...> flatMap(t -> Aggregate.<Integer,Circle>of(
   ...>    s -> Pair.<Integer,Circle>of(
   ...>       s + 3, new Circle(t))))
$.. ==> (16, Circle of radius 8.0)

You will need to tweak the signature of flatMap slightly to pass the last test.