Practice PE2 Question: Try
Adapted from PE2 of 20/21 Semester 2
Instructions to grab Practice PE Question:
- Accept the practice question here
- Log into the PE nodes and run
~cs2030s/get-pe2-try
to get the practice question. - There is no submission script
You should see the following in your home directory.
- The files
Test1.java
,Test2.java
,Test3.java
,Test4.java
, andCS2030STest.java
for testing your solution. - The skeleton file for this question:
cs2030s/fp/Try.java
. - The following files are also provided:
Circle.java
,Point.java
,Consumer.java
,Runnable.java
,Producer.java
, andTransformer.java
Background
In Java, we handle exceptions with try
and catch
. For example,
1 2 3 4 5 6 |
|
When we code with the functional paradigm, however, we prefer to chain our operations and keep our functions pure. A more functional way to write this block of code is to use the Try
monad:
1 |
|
The Try
monad is a way to encapsulate the result of the computation if it is successful, or the reason for failure if the computational failed. We refer to these two possibilities as success and failure respectively. In the example above, the Try<Circle>
instance would contain the new circle if it is a success, or an IllegalArgumentException
if it fails.
The reason for failure can be encapsulated as an instance of the Throwable
class. This class is defined in the java.lang
package and it is the parent class of Exception
and Error
. A Throwable
instance can be thrown and caught. Note that:
- cs2030s.fp.Producer::produce
and cs2030s.fp.Runnable::run
now throw a Throwable
.
- You don't need to call any methods or access any fields related to Throwable
beyond catching, throwing, and converting to string.
Your Task
You will implement the Try
monad in this question as part of the cs2030s.fp
package.
We break down the tasks you need to do into several sections. We suggest that you read through the whole question, plan your solution carefully before starting.
Please be reminded of the following:
-
You should design your code so that it is extensible to other possible states of computation in the future, beyond just success and failure.
-
Your code should be type-safe and catch as many type mismatches as possible during compile time.
Assumption
You can assume that everywhere a method of Try
accepts a functional interface or a Throwable
as a parameter, the argument we pass in will not be null
. When a value is expected, however, there is a possibility that we pass in a null
as an argument.
The Basics
First, please implement the following methods:
-
The
of
factory method, which allows us to create a newTry
monad with a producer of typeProducer
(imported from the packagecs2030s.fp.Producer
). Returns success if the producer produces a value successfully, or a failure containing the throwable if the producer throws an exception. -
The
success
factory method, which allows us to create a newTry
monad with a value. ATry
monad created this way is always a success. -
The
failure
factory method, which allows us to create a newTry
monad with aThrowable
. ATry
monad created this way is always a failure. -
Override the
equals
method inObject
so that it checks if two differentTry
instances are equal. TwoTry
instances are equal if (i) they are both a success and the values contained in them equal to each other, or (ii) they are both a failure and theThrowable
s contained in them have the same string representation. -
Implement the method
get()
.get()
returns the value if theTry
is a success. It throws theThrowable
if it is a failure.
Study carefully how these methods can be used in the examples below:
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 |
|
You can also test your code with Test1.java
:
1 2 3 4 |
|
Write the javadoc documentation for of
, success
, and failure
for Try
. Note that since we do not require you to write javadoc for every class and methods, checkstyle
no longer warns about missing javadoc for your class and methods.
map
Now, implement the map
method so that we can apply a computation on the content of Try
. If map
is called on a Try
instance that is a failure, the same instance of Try
is returned. Otherwise, if it is a success, the lambda expression is applied to the value contained within Try
. If this lambda expression throws a Throwable
, the calling Try
becomes a failure containing the Throwable
thrown.
Study carefully how map
can be used in the examples below:
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 also test your code with Test2.java
:
1 2 3 4 |
|
flatMap
Now, make Try
a monad. Implement the flatMap
method so that we can compose multiple methods that produce a Try
together. If flatMap
is called on a failure, return the failure. Otherwise, if it is a success, apply the lambda expression on the value contained within Try
and return the result.
Study carefully how map
can be used in the examples below:
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 |
|
You can also test your code with Test3.java
:
1 2 3 4 |
|
Dealing with failures
The methods map
and flatMap
apply the given lambda to the value contained within the Try
monad where it is a success. Write the following method to deal with failure:
onFailure
: Return this instance if the callingTry
instance is a success. Consume theThrowable
with aConsumer
if it is a failure, and then either (i) return this instance if the consumer runs successfully, or (ii) return a failure instance containing the error/exception when consuming theThrowable
.
For example, we can use onFailure
to replace this snippet
1 2 3 4 5 6 |
|
with:
1 2 |
|
We can also recover from the failure, by turning the Try
into a success. Write the following method:
recover
: Return this instance if it is a success. If thisTry
instance is a failure. Apply the givenTransformer
to theThrowable
, if the transformation is a success, return the resultingTry
, otherwise, return a failure containing the error/exception when transforming theThrowable
.
For example, we can use recover
to replace this snippet
1 2 3 4 5 6 |
|
with:
1 2 |
|
Study carefully how onFailure
and recover
can be used in the examples below:
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 |
|
You can also test your code with Test4.java
:
1 2 3 4 |
|
Write the javadoc documentation for onFailure
and recover
for Try
.