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 interface in Try.java to encapsulate a computation with the following methods:

  • 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.of(() 1) $.. Success: 1

jshell> Try.of(() “ok”) $.. Success: ok

jshell> Try.of(() { …> throw new IllegalStateException(“error”); }) $.. Failure: java.lang.IllegalStateException: error

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.of(() 1) $.. Success: 1

jshell> Try.of(() 1).of(() 2) | Error: | illegal static interface method call | the receiver expression should be replaced with the type qualifier ‘Try<java.lang.Integer>’ | Try.of(() 1).of(() 2) | ^----------------------------------^

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 t1 = Try.of(() 1) t1 Success: 1

jshell> Try t2 = Try.of(() { …> throw new IllegalStateException(“error”); }) t2 Failure: java.lang.IllegalStateException: error

jshell> t1.map(x x + 3) $.. Success: 4

jshell> t2.map(x x + 3) $.. Failure: java.lang.IllegalStateException: error

jshell> t1.flatMap(x Try.of(() x + 3)) $.. Success: 4

jshell> t1.flatMap(x Try.of(() { …> throw new IllegalStateException(“out of time”); })) $.. Failure: java.lang.IllegalStateException: out of time

jshell> t2.flatMap(x Try.of(() { …> throw new IllegalStateException(“out of time”); })) $.. Failure: java.lang.IllegalStateException: error

jshell> t2.flatMap(x Try.of(() 3)) $.. Failure: java.lang.IllegalStateException: error

jshell> Function<Object,Integer> f = x x.hashCode() f $Lambda..

jshell> Try tn = Try.of(() “OK”).map(f) tn Success: 2524

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 t1 = Try.of(() 1) t1 Success: 1

jshell> Try t2 = Try.of(() { …> throw new IllegalStateException(“error”); }) t2 Failure: java.lang.IllegalStateException: error

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> id = x Try.of(() x) id $Lambda..

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> ff = x Try.of(() x + 3) ff $Lambda..

jshell> Function<Integer,Try> gg = x Try.of(() x / 0) gg $Lambda..

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.of(() 1).tryCatch(x x + 10, ex 100) $.. 11

jshell> Try.of(() 1 / 0).tryCatch(x x + 10, ex 100) $.. 100

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 { private final List list; private final int top;

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 t5 = new TopN(5). …> add(“Liverpool”). …> add(“Man City”). …> add(“Arsenal”). …> add(“Man United”). …> add(“Aston Villa”) t5 [Liverpool, Man City, Arsenal, Man United, Aston Villa]

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> t5 = Try.of(() new TopN(5)). …> flatMap(t t.add(“Liverpool”)). …> flatMap(t t.add(“Man City”)). …> flatMap(t t.add(“Arsenal”)). …> flatMap(t t.add(“Man United”)). …> flatMap(t t.add(“Aston Villa”)) t5 Success: [Liverpool, Man City, Arsenal, Man United, Aston Villa]

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”