Try Context for Exception Handling
Topic Coverage
- Computation context
- Exception handling
- Local classes and variable captures
Programming languages such as Scala supports the Try monad (or context), which encapsulates a computation that either completes successfully with a returned value, or fails with an error message. With the Try monad, instead of printing out error messages when the error occur, causing the function to be impure, we can now return this error message as part of the return value. Furthermore, rather than returning a “special” value (such as null or -1) to indicate errors which are prone to bugs, using Try eliminates the need to return these “special” values.
You are given the following Try_skel.java source, which contains the Try interface, together with implementation classes Success and Failure.
More interestingly, the astute reader may notice that Try and Success (also Failure) are cyclic dependent! In this task, we shall remove the cyclic dependencies by implementing the success and failure classes as local classes enclosed within the Try interface.
This task is divided into several levels. You are required to design a single Try interface for levels 1 to 3. In level 4, you will implement another generic class called TopN that makes use of Try.
Level 1
Define a generic Try
- static
of(Supplier<? extends T> supplier) method that initializes a successful Try object with the given computation. - toString() method that returns either the string representation of the value if the Try object is a success, or the error message if it is a failure.
jshell> Try.
jshell> Try.
jshell> Try.
Specifically, the of method should call a private factory method (say succ) to create a successful Try, and another factory method (say fail) to create a failure Try.
interface Try
static <T> Try<T> of(Supplier<? extends T> supplier) {
...
}
private static <T> Try<T> succ(T val) {
return new Try<T>() {
// define methods using the captured variable (val)
};
}
private static <T> Try<T> fail(Exception exc) {
return new Try<T>() {
// define methods using the captured variable (exc)
};
}
}
Something worth pondering… the of method is defined as a static method within an interface so as to restrict the invocation at the start of pipeline creation, and not as an intermediate operation.
jshell> Try.
jshell> Try.
You would be surprised that Java’s Optional class does not prevent this from happening since Optional is a class, and static methods of a class can be invoked from any object.
jshell> Optional.of(1).map(x → x + 1) $.. ⇒ Optional[2]
jshell> Optional.of(1).map(x → x + 1).of(10) $.. ⇒ Optional[10]
Level 2
Now, implement the map and flatMap methods. You will first need to specify both of these methods in the Try interface.
public <R> Try<R> map(Function<? super T, ? extends R> mapper);
public <R> Try<R> flatMap(Function<? super T, ? extends Try<? extends R>> mapper);
If the Try object is a failure, these methods simply return the calling Try object; otherwise, apply the given function to the value contained within the Try object. Moreover, take note of the following:
- in the case of map, the given function is assumed pure, and hence does not throw an exception;
- in the case of flatMap, the given function can potentially throw an error, but it is nonetheless pure (since the error is encapsulated in the Try context).
Indeed, one is always expected to consciously convert all impure functions to pure ones in advance, and just use purely functional code.
jshell> Try
jshell> Try
jshell> t1.map(x → x + 3) $.. ⇒ Success: 4
jshell> t2.map(x → x + 3) $.. ⇒ Failure: java.lang.IllegalStateException: error
jshell> t1.flatMap(x → Try.
jshell> t1.flatMap(x → Try.
jshell> t2.flatMap(x → Try.
jshell> t2.flatMap(x → Try.
jshell> Function<Object,Integer> f = x → x.hashCode() f ⇒ $Lambda..
jshell> Try
Level 3
To facilitate the check for compliance against the functor and monad laws, implement a static equals method in the Try interface that takes in two Try objects and returns true if
- two successful Try objects encapsulates the same value, or
- two failure Try objects encapsulates exceptions with the same message.
jshell> Try
jshell> Try
jshell> Try.equals(t1, t1.map(x → x)) $.. ⇒ true
jshell> Try.equals(t2, t2.map(x → x)) $.. ⇒ true
jshell> Try.equals(t1, t2.map(x → x)) $.. ⇒ false
jshell> Try.equals(t2, t1.map(x → x)) $.. ⇒ false
jshell> Function<Integer,Integer> f = x → x + 3 f ⇒ $Lambda..
jshell> Function<Integer,Integer> g = x → x * 3 g ⇒ $Lambda..
jshell> Try.equals(t1.map(f).map(g), t1.map(g.compose(f))) $.. ⇒ true
jshell> Function<Integer,Try
jshell> Try.equals(t1.flatMap(id), t1) $.. ⇒ true
jshell> Try.equals(t2.flatMap(id), t2) $.. ⇒ true
jshell> id.apply(5) $.. ⇒ Success: 5
jshell> Function<Integer,Try
jshell> Function<Integer,Try
jshell> Try.equals(id.apply(5).flatMap(ff), ff.apply(5)) $.. ⇒ true
jshell> Try.equals(id.apply(5).flatMap(gg), gg.apply(5)) $.. ⇒ true
jshell> Try.equals(id.apply(5).flatMap(ff), gg.apply(5)) $.. ⇒ false
jshell> Try.equals(t1.flatMap(ff).flatMap(gg), t1.flatMap(x → ff.apply(x).flatMap(gg))) $.. ⇒ true
jshell> Try.equals(t1.flatMap(gg).flatMap(ff), t1.flatMap(x → gg.apply(x).flatMap(ff))) $.. ⇒ true
Hint: define the tryCatch method to assist with “getting” the value (or exception) encapsulated within the Try.
public <R> R tryCatch(Function<? super T, ? extends R> succ, Function<Exception, ? extends R> fail);
Here is an example of what tryCatch can do.
jshell> Try.
jshell> Try.
Here’s an advice… since tryCatch can be exploited as a getter, one should avoid using it indiscriminately.
Level 4
The Try class can be used in many common situations. You are given a skeleton generic class TopN in TopN_skel.java.
import java.util.List;
class TopN
TopN(int top) {
this(List.<T>of(), top);
}
private TopN(List<T> list, int top) {
this.list = list;
this.top = top;
}
TopN<T> add(T str) {
return this;
}
T get(int i) {
return list.get(i);
}
int find(T s) {
return 0;
}
public String toString() {
return this.list.toString();
}
}
Let’s throw some exceptions first. Rewrite the class in TopN.java following the sample run below.
jshell> TopN
jshell> t5.get(0) $.. ⇒ “Liverpool”
jshell> t5.get(13) | Exception java.lang.IllegalStateException: index 13 out of range | at ..
jshell> t5.add(“Chelsea”) | Exception java.lang.IllegalStateException: out of range | at ..
jshell> t5.find(“Arsenal”) $.. ⇒ 2
jshell> t5.find(“Chelsea”) | Exception java.lang.IllegalStateException: Chelsea not among top 5 | at ..
Now further modify TopN so that the methods add, get, and find returns a Try object so that success or failure is encapsulated within the context. You only need to encapsulate the original return values within a Try. More specifically, wrap each method implementation in a supplier, wrap that in a Try, and have that returned in each method.
All previous operations on TopN can now be passed declaratively via flatMap to the Try context instead.
jshell> Try<TopN
jshell> t5.flatMap(t → t.get(0)) $.. ⇒ Success: Liverpool
jshell> t5.flatMap(t → t.get(0)).map(team → team + ” is at the top”) $.. ⇒ Success: Liverpool is at the top
jshell> t5.flatMap(t → t.get(13).map(team → team + ” is at 14th place”)) $.. ⇒ Failure: java.lang.IllegalStateException: index 13 out of range
jshell> t5.flatMap(t → t.add(“Chelsea”)) $.. ⇒ Failure: java.lang.IllegalStateException: out of range
jshell> t5.flatMap(t → t.find(“Arsenal”)).map(i → “Arsenal is at pos ” + i) $.. ⇒ Success: Arsenal is at pos 2
jshell> t5.flatMap(t → t.find(“Chelsea”)).map(i → “Chelsea is at pos ” + i) $.. ⇒ Failure: java.lang.IllegalStateException: Chelsea not among top 5
jshell> t5.flatMap(t → t.find(“Man City”)). …> tryCatch(x → “ranked ” + (x + 1) + ” on the table”, ex → “bottom of the table”) $.. ⇒ “ranked 2 on the table”
jshell> t5.flatMap(t → t.find(“Chelsea”)). …> tryCatch(x → “ranked ” + (x + 1) + ” on the table”, ex → “nowhere near the top”) $.. ⇒ “nowhere near the top”