Ex 5: Call Me Maybe
Basic Information
- Deadline: 15 October 2024, Tuesday, 23:59 SGT
- Difficulty: ★★★★★★
Prerequisite
- Caught up to Unit 29 of Lecture Notes.
- Familiar with CS2030S Java style guide.
Goal
This is a continuation of Programming Exercise 4. In Exercise 4, we have constructed a generic class Some<T>, which is a container for an item of type T. Beyond being an exercise for teaching about generics, Some<T> is not a very useful type. In Programming Exercises 5 and 6, we are going to modify Some<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 and Arrays from as well as 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. This is another advantage of public class. The previous advantage of a public class is that it must have the same name as the file. So java compiler knows where to search. 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.
As a summary, the access levels are as follows.
| Modifier | Access from same class | Access from same package (or same directory) |
Access from subclass (even in other directory) |
Access from other class (even in other directory) |
|---|---|---|---|---|
public |
||||
protected |
||||
| no modifier | ||||
private |
We will create a package named cs2030s.fp to be used for this and the next few exercises.
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 one interface Transformer that you have written in Programming Exercise 4.
If you have not made Transformer a public class, you should do it now.
1 2 3 | |
Finally, to compile your code, under your ex5-username directory, run:
1 | |
Note, you may see some compilation error because the java files in ex5-username directory may fail to compile. That is normal. TestN.java can only compile when you finish some tasks below. If you have set up everything correctly, you should be able to run the following in JShell from your ex5-username directory:
1 | |
Checkpoint
If you have done this correctly, your directory structure should look something like the following:
1 2 3 4 5 6 7 8 9 10 | |
Tasks
Eventually, we will be creating a static nested class Some<T> that is nested inside the Maybe<T> class.
Maybe<T> encapsulates the possibility that a value is missing.
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.
Task 1: More Interfaces
Now, we are going to add three more interfaces into our package:
Producer<T>is an interface with a singleproducemethod that takes in no parameter and returns a value of typeT.Consumer<T>is an interface with a singleconsumemethod that takes in a parameter of typeTand returns nothing.BooleanCondition<T>is an interface with a singletestmethod that takes in a parameter of typeTand returns a primitivebooleanvalue.
Checkpoint
If you have done this correctly, your directory structure should look something like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
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!).
| Sample Usage | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
Task 2: Some Packaging
There is minimal amount of code to be added here. We will be mainly be doing a rearrangement of code.
- Copy your implementation of
Some.javaintolab5-username/cs2030s/fpdirectory if you have not done so. - Add
package cs2030s.fp;as the first line onSome.java. -
Rename
Some.javaintoMaybe.java. This entails some other changes too:- Rename some occurrences of
SomeintoMaybeespecially the return types. - Do NOT change the name of the factory method
some.
Checkpoint
If you have done this correctly, your directory structure should look something like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13
labX-username/ ├─ cs2030s/ │ └─ fp/ │ ├─ BooleanCondition.java │ ├─ Consumer.java │ ├─ Maybe.java │ ├─ Producer.java │ └─ Transformer.java ├─ CS2030STest.java ├─ Test1.java ├─ Test2.java ├─ : └─ ...Your
Maybe.javashould still contain the classSome<T>.cs2030s/fp/Maybe.java 1 2 3
public class Some<T> { : } - Rename some occurrences of
-
Change
public class Some<T>toprivate static final class Some<T> extends Maybe<T>.- Then wrap it inside the outer class
public abstract class Maybe<T>. - Move
public static <T> Maybe<T> some(T value)fromSome<T>toMaybe<T>.
Checkpoint
At this point,
Some<T>is a static nested class insideMaybe<T>. But codes from outside of the package cannot seeSome<T>and onlyMaybe<T>. SinceMaybe<T>does not have any known method, we need to add abstract methods.cs2030s/fp/Maybe.java 1 2 3 4 5 6 7 8 9 10
public abstract class Maybe<T> { : public static <T> Maybe<T> some(T value) { : // implementation omitted } private static final class Some<T> extends Maybe<T> { : // code from the old version of Some.java } } - Then wrap it inside the outer class
-
Add abstract method descriptor that appears in
Some<T>toMaybe<T>unless these method descriptor already available inObject. -
Finally, we need to handle
nullvalues inSome<T>andMaybe<T>.public static <T> Maybe<T> some(T value)acceptsnulland simply store thenullvalue in the field.- Two
Some<T>instances are equal (as decided by their respectiveequals(Object)method) if either one (or both) of the following condition is true.- The content are both
null. - The content are equal as decided by their respective
equals(Object)method.
- The content are both
map
There is no need to specially handle
nullinmap. In particular, if theTransfomerinmapreturnsnull, we will simply use thenullvalue.
| Sample Usage | |
|---|---|
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 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | |
You can test this more comprehensively by running without compilation warning/error and all tests printing ok.
Make sure your code follows the CS2030S Java style.
| Test1.java | |
|---|---|
1 2 3 | |
Task 3: None Other than You
Now we want to add None<T> class as another private static nested class inside Maybe<T>.
This class is also a subtype of Maybe<T>.
The types None<T> is an internal implementation details of Maybe<T> and must not be used directly by the client. Hence, it must be declared private. Here is the requirement for None<T>.
None<T>is a generic private inner class that inherits fromMaybe<T>.None<T>has no instance field.None<T>has private constructor that takes in no argument.None<T>overrides theequals(Object)method.- Any instance of
None<T>is equal to any other instance ofNone<T>. - Note that
Some<T>should never be equal toNone<T>.
- Any instance of
None<T>overrides thetoString()method.- It simply prints
[].
- It simply prints
None<T>overrides themapmethod fromMaybe<T>.- This simply returns itself.
None<T>(and by extensionMaybe<T>) must be immutable up toT.- But you do not have to make the class a
finalclass.
- But you do not have to make the class a
Additionally, we need to add the following factory methods in Maybe<T>.
- Add the factory method
none()that returns an instance ofNone<T>.- There should only be ONE instance of
None<T>such that multiple calls tonone()should return the same instance. - You may add
@SuppressWarningshere with explanation on why it is safe.
- There should only be ONE instance of
- Add the factory method
ofthat returns:- an instance of
None<T>if the input isnull. - an instance of
Some<T>if the input is notnull.
- an instance of
| Sample Usage | |
|---|---|
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 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | |
You can test this more comprehensively by running without compilation warning/error and all tests printing ok.
Make sure your code follows the CS2030S Java style.
| Test2.java | |
|---|---|
1 2 3 | |
Task 4: Filtering
We now add the method filter to Maybe<T>.
- Add the abstract method
filtertoMaybe<T>that takes in aBooleanCondition<..>(type parameter is omitted) as a parameter. - Override the method
filterinSome<T>as follows.- If the value is
nullor it failed the test (i.e., the call totestreturnsfalse), returnNone<T>. - Otherwise, leaves the
Maybe<T>untouched and returns theMaybe<T>as it is.
- If the value is
- Override the method
filterinNone<T>as follows.- Always returns a
None<T>.
- Always returns a
| Sample Usage | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
You can test this more comprehensively by running without compilation warning/error and all tests printing ok.
Make sure your code follows the CS2030S Java style.
| Test3.java | |
|---|---|
1 2 3 | |
Task 5: 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. We want to add a method that is not doing this.
- Add the abstract method
flatMaptoMaybe<T>that takes in aTransfomer<..>(type parameter is omitted) as a parameter. - Override the method
flatMapinSome<T>as follows.- The
Transformerobject transforms the value of typeTinMaybe<T>into a value of typeMaybe<U>, for some typeU. - The method
flatMap, however, returns a value of typeMaybe<U>(instead ofMaybe<Maybe<U>>as in the case ofmap). - You may add
@SuppressWarningshere with explanation on why it is safe.
- The
- Override the method
flatMapinNone<T>as follows.- Always returns a
None<T>.
- Always returns a
| Sample Usage | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
You can test this more comprehensively by running without compilation warning/error and all tests printing ok.
Make sure your code follows the CS2030S Java style.
| Test4.java | |
|---|---|
1 2 3 | |
Task 6: Back to T
Since Maybe<T> is an abstraction for a possibly missing value (of type T), it would be useful to provide methods that decide what to do if the value is missing.
- Add the abstract method
orElseinMaybe<T>that takes in aProducer<..>(type parameter is omitted) as the parameter.- Override the method
orElseinSome<T>to return the value inside. - Override the method
orElseinNone<T>to return the subtype ofTproduced by the producer.
- Override the method
- Add the abstract method
ifPresentinMaybe<T>that takes in aConsumer<..>(type parameter is omitted) as the parameter.- Override the method
ifPresentinSome<T>such that the given consumer consumes the value inside. - Override the method
ifPresentinNone<T>that does nothing.
- Override the method
| Sample Usage | |
|---|---|
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 | |
You can test this more comprehensively by running without compilation warning/error and all tests printing ok.
Make sure your code follows the CS2030S Java style.
| Test5.java | |
|---|---|
1 2 3 | |
Skeleton for Programming Exercise 5
You need to copy the files Some.java and Transformer.java from ex4-username to ex5-username.
Some files (e.g., Test1.java, Test2.java, CS2030STest.java, etc) are provided for testing.
Do not copy these from ex4-username.
You may edit them to add your own test cases, but we will be using our own version for testing.
While there is no given public test cases for it, we will test your code with hidden test cases that checks for flexible type.
Additionally, minimize the number of type parameter by using wildcards.
Lastly, ensure that you use @SuppressWarnings as needed.
Following CS2030S Style Guide
You should make sure your code follows the given Java style guide.
To check for style, we may need two commands as there are two directories of interest.
| Style Check | |
|---|---|
1 2 | |
Further Deductions
Additional deductions may be given for other issues or errors in your code. These deductions may now be unbounded, up to 5 marks. This include but not limited to
- run-time error.
- failure to follow instructions.
- improper designs (e.g., not following good OOP practice).
- not comenting
@SuppressWarnings. - misuse of
@SuppressWarnings(e.g., not necessary, not in smallest scope, etc).
Documentation (Optional)
Documenting your code with Javadoc is optional for Programming Exercise 5. It is, however, always a good practice to include comments to help readers understand your code.