$30
In this lab, we are going to write an immutable Logger class to handle the context of logging changes to values while they are operated upon. In a way, the logger separates the code for logging from the main processing. Each logger wraps around a value, and everytime map or flatMap is called, a new Logger object is created. The printlog method can be invoked the output the log.
A sample logging session is shown below:
jshell> Logger.make(5)
$.. ==> Logger[5]
jshell> Logger.make(5).printlog()
Value initialized. Value = 5
jshell> Logger.make(5).map(x -> x + 1)
$.. ==> Logger[6]
jshell> Logger.make(5).map(x -> x + 1).printlog()
Value initialized. Value = 5
Value changed! New value = 6
jshell> Logger.make(5).map(x -> x * 1)
$.. ==> Logger[5]
jshell> Logger.make(5).map(x -> x * 1).printlog()
Value initialized. Value = 5
Value unchanged. Value = 5
Notice that the log of value changes through time is output whenever the printlog() method is called. There are three different types of log messages:
During initialization: Value initialized. Value = ..
When value is modified: Value changed! New value = ..
When value remains unchanged: Value unchanged. Value = ..
Task
Your task is to write a Logger class that provides the operations make, equals, printlog, map, flatMap and test. You will also write several applications using the Logger as solutions to classic computation problems. This would allow us to look at the values changes when solving each problem.
This task is divided into several levels. Read through all the levels to see how the different levels are related.
Remember to:
always compile your program files first before using jshell to test your program
use checkstyle and javadoc comments to enhance code readability and facilitating code review
Level 1
Creating the logger
Start by writing a static method make to wrap a value within a Logger. Include the toString() method as well as the printlog() method to return the string representation of the Logger and output the log messages respectively. At this point of time, there are only initialization messages. Finally, include an equals method that returns true if the argument Logger object is the same as this, or false otherwise. Two Loggers are equal if and only if both the wrapped value as well as the logs are the same.
jshell> Logger.make(5)
$.. ==> Logger[5]
jshell> Logger.make(5).printlog()
Value initialized. Value = 5
jshell> Logger.make("hello")
$.. ==> Logger[hello]
jshell> Logger.make("hello").printlog()
Value initialized. Value = hello
jshell> Logger.make(5).equals(Logger.make(5))
$.. ==> true
jshell> Logger.make(5).equals(5)
$.. ==> false
jshell> Logger.make(5).equals(Logger.make("five"))
$.. ==> false
jshell> Logger.make(5).equals((Object)(Logger.make(5)))
$.. ==> true
jshell> /exit
Check the format correctness of the output by running the following on the command line:
$ javac -Xlint:rawtypes *.java
$ jshell -q your_java_files_in_bottom-up_dependency_order < test1.jsh
Check your styling by issuing the following
$ checkstyle *.java
Level 2
Creating a valid logger
We need to prevent the make method from being misused by nesting or chaining the methods, e.g.
Logger.make(Logger.make(5))
Logger.make(5).make(7)
In the case of nesting make methods, we throw an IllegalArgumentException everytime the make method takes in another Logger as argument.
To prevent chaining, we can make the Logger an interface with the static method make, and let LoggerImpl be the concrete class that implements the functionalities of logging. Here, we restrict the chaining of make method calls at the expense of introducing a cyclic dependency between Logger and LoggerImpl... that is, unless someone comes up with a better idea...
In addition, passing a null to make will also result in an IllegalArgumentException thrown.
jshell> Logger.make(5)
$.. ==> Logger[5]
jshell> Logger.make(5) instanceof Logger
$.. ==> true
jshell> try { Logger.make(Logger.make(7)); } catch (Exception e) { System.out.println(e); }
java.lang.IllegalArgumentException: already a Logger
jshell> Logger.make(5).make(7)
| Error:
| illegal static interface method call
| the receiver expression should be replaced with the type qualifier 'Logger<java.lang.Integer>'
| Logger.make(5).make(7)
| ^--------------------^
jshell> try { Logger.make(null); } catch (Exception e) { System.out.println(e); }
java.lang.IllegalArgumentException: argument cannot be null
jshell> /exit
Check the format correctness of the output by running the following on the command line:
$ javac -Xlint:rawtypes *.java
$ jshell -q your_java_files_in_bottom-up_dependency_order < test2.jsh
Check your styling by issuing the following
$ checkstyle *.java
Level 3
The map method
Include a map method that takes in a function of the form Function<? super T, ? extends U>, applies it on the value, and wraps the result in a Logger.
jshell> Logger.make(5)
$.. ==> Logger[5]
jshell> Logger.make(5).printlog()
Value initialized. Value = 5
jshell> Logger.make(5).map(x -> x + 1)
$.. ==> Logger[6]
jshell> Logger.make(5).map(x -> x + 1).printlog()
Value initialized. Value = 5
Value changed! New value = 6
jshell> Logger<Integer> a = Logger.make(5)
jshell> a.printlog()
Value initialized. Value = 5
jshell> a.map(x -> x + 1)
$.. ==> Logger[6]
jshell> a.printlog()
Value initialized. Value = 5
jshell> Logger.make(5).map(x -> x)
$.. ==> Logger[5]
jshell> Logger.make(5).map(x -> x).printlog()
Value initialized. Value = 5
Value unchanged. Value = 5
jshell> Logger.make(5).equals(Logger.make(5).map(x -> x))
$.. ==> false
jshell> Logger.make(5).map(x -> x).equals(Logger.make(5))
$.. ==> false
jshell> Logger.make(5).map(x -> x + 1).map(x -> x - 1)
$.. ==> Logger[5]
jshell> Logger.make(5).map(x -> x + 1).map(x -> x - 1).printlog()
Value initialized. Value = 5
Value changed! New value = 6
Value changed! New value = 5
jshell> Logger.make(5).map(x -> x + 1).map(x -> x - 1).equals(Logger.make(5))
$.. ==> false
jshell> Logger.make("hello").map(String::length)
$.. ==> Logger[5]
jshell> Logger.make("hello").map(String::length).printlog()
Value initialized. Value = hello
Value changed! New value = 5
jshell> Function<Object, Boolean> f = x -> x.equals(x)
jshell> Logger.make("hello").map(f)
$.. ==> Logger[true]
jshell> Function<String, Number> g = x -> x.length();
jshell> Logger<Number> lognum = Logger.make("hello").map(g)
jshell> lognum
lognum ==> Logger[5]
jshell> /exit
Check the format correctness of the output by running the following on the command line:
$ javac -Xlint:rawtypes *.java
$ jshell -q your_java_files_in_bottom-up_dependency_order < test3.jsh
Check your styling by issuing the following
$ checkstyle *.java
Level 4
The flatMap method
We are now ready to write the flatMap method that takes a function of the form Function<? super T, ? extends Logger<? extends U>>. Pay special attention to how the logs are to be combined.
jshell> Logger.make(5).printlog()
Value initialized. Value = 5
jshell> Logger.make(5).flatMap(x -> Logger.make(x))
$.. ==> Logger[5]
jshell> Logger.make(5).flatMap(x -> Logger.make(x)).printlog()
Value initialized. Value = 5
jshell> Logger.make(5).flatMap(x -> Logger.make(x)).equals(Logger.make(5))
$.. ==> true
jshell> Logger<Integer> a = Logger.make(5).flatMap(x -> Logger.make(x).map(y -> y + 2)).flatMap(y -> Logger.make(y).map(z -> z * 10))
jshell> Logger<Integer> b = Logger.make(5).flatMap(x -> Logger.make(x).map(y -> y + 2).flatMap(y -> Logger.make(y).map(z -> z * 10)))
jshell> a.printlog()
Value initialized. Value = 5
Value changed! New value = 7
Value changed! New value = 70
jshell> b.printlog()
Value initialized. Value = 5
Value changed! New value = 7
Value changed! New value = 70
jshell> a.equals(b)
$.. ==> true
jshell> Logger<Integer> c = Logger.make(5).map(x -> x + 2).map(x -> x * 10)
jshell> a.equals(c)
$.. ==> true
jshell> Function<Object, Logger<Boolean>> f = x -> Logger.make(x).map(y -> y.equals(y))
jshell> Logger.make("hello").flatMap(f)
$.. ==> Logger[true]
jshell> Function<String, Logger<Number>> g = x -> Logger.make(x).map(y -> y.length())
jshell> Logger<Number> lognum = Logger.make("hello").flatMap(g)
jshell> lognum
lognum ==> Logger[5]
jshell> /exit
Check the format correctness of the output by running the following on the command line:
$ javac -Xlint:rawtypes *.java
$ jshell -q your_java_files_in_bottom-up_dependency_order < test4.jsh
Check your styling by issuing the following
$ checkstyle *.java
Level 5
Let's write some applications using JShell that makes use of our Logger so as to observe how the values changes over the course computation. Save your methods in the file level5.jsh.
Define an add(Logger<Integer> a, int b) method that returns the result of a added to b wrapped in a Logger that preserves the log of all operations of a, as well as the addition to b.
jshell> add(Logger.make(5), 6)
$.. ==> Logger[11]
jshell> add(Logger.make(5), 6).printlog()
Value initialized. Value = 5
Value changed! New value = 11
jshell> add(Logger.make(5).map(x -> x * 2), 6)
$.. ==> Logger[16]
jshell> add(Logger.make(5).map(x -> x * 2), 6).printlog()
Value initialized. Value = 5
Value changed! New value = 10
Value changed! New value = 16
The sum of non-negative integers from 0 to n (inclusive of both) can be computed recursively using
int sum(int n) {
if (n == 0) {
return 0;
} else {
return sum(n - 1) + n;
}
}
Redefine the above method such that it returns the result wrapped in a Logger. You may find the above add method useful.jshell> sum(0)
$.. ==> Logger[0]
jshell> sum(0).printlog()
Value initialized. Value = 0
jshell> sum(5)
$.. ==> Logger[15]
jshell> sum(5).printlog()
Value initialized. Value = 0
Value changed! New value = 1
Value changed! New value = 3
Value changed! New value = 6
Value changed! New value = 10
Value changed! New value = 15
The Collatz conjecture (or the 3n+1 Conjecture) is a process of generating a sequence of numbers starting with a positive integer that seems to always end with 1.
int f(int n) {
if (n == 1) {
return 1;
} else if (n % 2 == 0) {
return f(n / 2);
} else {
return f(3 * n + 1);
}
}
Write the function f that takes in n and returns a Logger<Integer> that wraps around the final value, with a log of the value changes over time. You should include a test method in the Logger that takes in a Predicate and returns true if the value within the Logger satisfies the predicate, and false otherwise.
jshell> Logger.make(5).test(x -> x == 5)
$.. ==> true
jshell> Logger.make(5).map(x -> x + 1).test(x -> x % 2 != 0)
$.. ==> false
jshell> Logger.make("hello").test(x -> x.length() == 5)
$.. ==> true
jshell> f(16)
$.. ==> Logger[1]
jshell> f(16).printlog()
Value initialized. Value = 16
Value changed! New value = 8
Value changed! New value = 4
Value changed! New value = 2
Value changed! New value = 1
jshell> f(10)
$.. ==> Logger[1]
jshell> f(10).printlog()
Value initialized. Value = 10
Value changed! New value = 5
Value changed! New value = 15
Value changed! New value = 16
Value changed! New value = 8
Value changed! New value = 4
Value changed! New value = 2
Value changed! New value = 1
Check the format correctness of the output by running the following on the command line:
$ javac -Xlint:rawtypes *.java
$ jshell -q your_java_files_in_bottom-up_dependency_order < test5.jsh
Check your styling by issuing the following
$ checkstyle *.java