Exercise 5: Maybe
- Deadline: 19 March, 2023, Tuesday, 23:59, SST
- Difficulty Level: 6
Prerequisite:
- Caught up to Unit 29 of Lecture Notes
- Familiar with the CS2030S Java style guide
This is a follow-up to Exercise 4. In Exercise 4, we have constructed a generic class Box<T>
, which is a container for an item of type T
. Beyond being an exercise for teaching about generics, Box<T>
is not a very useful type. In Exercises 5 and 6, we are going to modify Box<T>
into two more useful and general classes. We are going to build our own Java packages using these useful classes.
Java package
Java package mechanism allows us to group relevant classes and interfaces under a namespace. You have seen two packages so far: java.util
, where we import List
, Arrays
, from, and java.lang
where we import the Math
class. These are provided by Java as standard libraries. We can also create our package and put the classes and interfaces into the same package. We (and the clients) can then import and use the classes and interfaces that we provide.
Java package provides a higher layer of abstraction barrier. We can designate a class to be used outside a package by prefixing the keyword class
with the access modifier public
. We can further fine-tune which fields and methods are accessible from other classes in the same package using the protected
access modifier.
You can read more about java packages and the protected
modifier yourself through Oracle's Java tutorial.
We will create a package named cs2030s.fp
to be used for Exercises 5-7.
First, we need to add the line:
1 |
|
on top of every .java
file that we would like to include in the package.
Second, the package name is typically written in a hierarchical manner using the "." notation. The name also indicates the location of the .java
files and the .class
files. For this reason, you can no longer store the .java
files under ex5-username
directly. Instead, you should put them in a subdirectory called cs2030s/fp
under ex5-username
.
To start, our cs2030s.fp
package will contain the two interfaces Transformer
and BooleanCondition
that you have written in Exercise 4.
Third, the classes or interfaces that you want to expose to others should be declared as a public
class or interface. You can do so by adding the keyword public
when you declare the class or interface. For example,
1 2 3 |
|
Finally, to compile your code, under your ex5-username
directory, run:
1 |
|
If you have set up everything correctly, you should be able to run the following in jshell
from your ex5-username
directory:
1 2 |
|
without error.
More Interfaces
Now, add two more interfaces to our package:
Producer<T>
is an interface with a singleproduce
method that takes in no parameter and returns a value of typeT
.Consumer<T>
is an interface with a singleconsume
method that takes in a parameter of typeT
and returns nothing.
If you have set up everything correctly, you should be able to run the following in jshell
without errors (remember to always compile your code first!)
1 2 3 4 5 6 7 8 9 10 |
|
Call Me Maybe<T>
Now, we are going to implement a type called Maybe<T>
in the cs2030s.fp
package. Our Maybe<T>
is an option type, a common abstraction in programming languages (java.util.Optional
in Java, option
in Scala, Maybe
in Haskell, Nullable<T>
in C#, etc) that is a wrapper around a value that might be missing. In other words, it represents either some value, or none.
Inner Classes and Factory Methods
Write an abstract class called Maybe<T>
with two concrete, static, nested classes, named None
and Some<T>
.
1 2 3 4 5 6 7 |
|
- Both
None
andSome<T>
inherit fromMaybe<T>
. Note thatNone
is not a generic class so you need to specifyObject
as the type argument toMaybe<T>
. Some<T>
must be immutable.- The types
None
andSome<T>
are internal implementation details ofMaybe<T>
and must not be used directly. For instance, clients must not be able to declare a variable of typeMaybe.Some<T>
.
1 2 3 4 5 6 7 8 9 10 11 |
|
Maybe<T>
has two static factory methods:
none()
returns an instance ofNone
. Just likeBox::empty
, there should only be one instance ofNone
. Multiple calls tonone()
should return the same instance.some(T t)
takes in a valuet
and returns an instance ofSome<T>
wrapped aroundt
(t
might benull
).
Implement a None::toString
method that always returns []
and a Some::toString
methods that always return the string representation of the content between [
and ]
.
Here are some examples of how the factory methods might be used (remember to always compile your code first!)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Implement the equal
method such that two None
are always equal, and two Some<T>
instances are equal if their contents are equal. (You might find your Box
implementation useful).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Finally, add a static factory of
, which returns an instance of Some
if the input is not null, and None
otherwise.
1 2 3 4 5 6 7 8 |
|
Add a protected
abstract method called get()
with a return type of T
into Maybe<T>
. Implement get()
in None
such that it throws a NoSuchElementException
, and in Some<T>
such that it returns the value contained inside. Since this method might throw an exception if the client misuses it, we keep the method protected
and usable only within our package
. Due to this, we can't test it. But keep this method in mind as it might be handy later.
1 2 3 4 5 6 7 8 9 10 |
|
You can test your code by running the Test1.java
provided. The following should compile without errors or warnings. Make sure your code follows the CS2030S Java style.
1 2 3 |
|
Filter and Map (again!)
We now add the methods filter
and map
to Maybe<T>
.
Create an abstract method filter
in Maybe<T>
that takes in a BooleanCondition<..>
(type parameter is omitted) as a parameter and implement this method in both None
and Some<T>
.
Calling filter
on None
always returns a None
. Calling filter
on Some
should return None
if the value in Some
is not null
and failed the test (i.e., the call to test
returns false
). Otherwise, filter
leaves the Maybe
untouched and returns the Maybe
as it is.
Here is how filter
could be used. Remember to always compile your code first before using it in `jshell``:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Create an abstract method map
in Maybe<T>
that takes in a Transformer<...>
(type parameter omitted) as a parameter. Implement map
in both None
and Some<T>
.
Calling map
on None
always returns a None
. Call map
on Some<T>
should return a new Some<T>
with the value inside transformed by the Transformer
instance. Note that, if the transform
method does not handle the case where the input is null
, a NullPointerException
will be thrown.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
|
You can test your code by running the Test2.java
provided. The following should compile without errors or warnings. Make sure your code follows the CS2030S Java style.
1 2 3 |
|
Remember to make your methods as flexible as they can be in the type that they accept.
flatMap
Consider a Transformer
that might return a Maybe<T>
itself (as wordToMaybeInt
above). Using map
on such a Transformer
would lead to a value wrapped around a Maybe
twice.
Create an abstract method flatMap
in Maybe<T>
(and implement it in both None
and Some<T>
) that takes in a Transfomer<..>
as the parameter. The Transformer
object transforms the value of type T
in Maybe<T>
into a value of type Maybe<U>
, for some type U
. The method flatMap
, however, returns a value of type Maybe<U>
(instead of Maybe<Maybe<U>>
as in the case of map
).
Remember to apply PECS in your method signature so that flatMap
is as flexible as possible. We no longer explicitly test for it in our public test cases.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
You can test your code by running the Test3.java
provided. The following should compile without errors or warnings. Make sure your code follows the CS2030S Java style.
1 2 3 |
|
Or Else
Since Maybe
is an abstraction for a possibly missing value, it would be useful to provide methods that decide what to do if the value is missing.
Add an abstract method orElse
in Maybe<T>
. Implement it in None
and Some<T>
, such that None
returns a given value that is a subtype of T
, while Some<T>
just returns the value inside. For example,
1 2 3 4 |
|
Add an abstract method orElseGet
in Maybe<T>
. orElseGet
takes in a producer. Implements orElseGet
in None
and Some<T>
, such that None
returns a value that is a subtype of T
produced by the producer, while Some<T>
just returns the value inside. For example,
1 2 3 4 5 6 7 8 9 |
|
Add an abstract method ifPresent
in Maybe<T>
. ifPresent
takes in a consumer. Implements ifPresent
in None
and Some<T>
, such that the given consumer does nothing for None
and consumes the value inside for Some<T>
.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
You can test your code by running the Test4.java
provided. The following should compile without errors or warnings. Make sure your code follows the CS2030S Java style.
1 2 3 |
|
Using Maybe
Now that we have our Maybe
class, let's try to use it to do something more meaningful.
It is a common idiom (although not a good one) for a method to return a value if successful and return a null
otherwise. It is up to the caller to check and make sure that the return value is not null
before using it, to prevent receiving a run-time NullPointerException
.
One example of this is the Map<K, V>
implemented in Java. You have seen above that Map::get
returns null
if the key that you are looking for does not exist.
We have given you a program Ex5.java
that uses multiple layers of Map
to store information about students, their modules, and their assessment grades. There is a method getGrade
that, given this map, a student, a module, and an assessment, look up the corresponding grade. There are multiple checks if a returned value is null
in this method.
Our new Maybe<T>
class provides a good abstraction for the returned value from Map::get
since the value returned is either some value or none!
Your final task is to modify getGrade
so that it uses Maybe<T>
instead:
- Declare and initialize two
Transformer
instances using anonymous classes. - Use the two
Transformers
,Maybe::of
,Maybe::flatMap
, andMaybe::orElse
to achieve the same functionality as the givengetGrade
in a single return statement. In other words, yourgetGrade
should consist of three Java statements: two to create two Transformers, and onereturn
statement. The skeleton has been given. - Your code should not have any more conditional statements or references to
null
.
Compile your edited Ex5
class. The following should compile without errors or warnings. Make sure your code follows the CS2030S Java style.
1 2 3 |
|
(if the file ex5_style.xml
is missing, you can copy the one you used from Exercise 4 over)
Files
A set of empty files has been given to you. You should only edit these files. You must not add any additional files.
The files Test1.java
, Test2.java
, etc., as well as CS2030STest.java
, are provided for testing. You can edit them to add your test cases, but they will not be submitted.
Following CS2030S Style Guide
You should make sure that your code follows the given Java style guide