Vavr One Log 03 - A Safe Try

Sealed types

It is an inadequacy of the previous Vavr version that Try was designed to be an interface. It allows 3rd party libraries to put their own implementations into the mix.

// Java
public interface Try<T> {

    // unsafe 😐
    T get() throws NoSuchElementException;
    
    // unsafe 😐
    Throwable getCause() throws UnsupportedOperationException;
}

public final class Success<T> implements Try<T> {

    // ...

    @Override
    public T get() {
        return value;
    }
    
    @Override
    public Throwable getCause() {
        throw new UnsupportedOperationException();
    }
}

public final class Failure<T> implements Try<T> {

    // ...
    
    @Override
    public T get() {
        throw new NoSuchElementException();
    }
    
    @Override
    public Throwable getCause() {
        return cause;
    }
}

Sum-types like Try are restricted to have a fixed number of implementations. Sealed types help us to enforce this on the source code level by disallowing additional implementations.

The following example sketches Try in Scala (incomplete):

// Scala
sealed abstract class Try[+T] {
    def get: T // Failure throws
    def isSuccess: Boolean
}

final case class Success[+T](value: T) extends Try[T] {
    override def get: T = value
    override def isSuccess: Boolean = true
}

final case class Failure[+T](exception: Throwable) extends Try[T] {
    override def get: T = throw exception
    override def isSuccess: Boolean = false
}

The abstract class is sealed in order to limit the implementations to Success and Failure. Because of sealed types, the Scala compiler knows that a pattern-match expression covers all cases and is therefore safe:

// Scala
val message = myTry match {
  case Success(value) => s"value: ${value}"
  case Failure(exception) => s"exception: ${exception}"
}

Beside that, the real value of Try is its dual nature. A computation can be either a Success or a Failure. There exists nothing else and sealed types help us to enforce this.

Java does not have a notion for sealed types. Our take in Vavr will look like this:

// Java
public abstract class Try<T> {
    Try() {
        if (!(this instanceof Success ||
              this instanceof Failure)) {
            throw new Error("Try is sealed");
        }
    }
}

public final class Success<T> extends Try<T> {}

public final class Failure<T> extends Try<T> {}

The constructor of Try needs to be visible because Success and Failure are public classes on the same package. Theoretically we could declare the same package in a different project and define our own subtype of Try. We prevent this by performing type-checks on each instantiation of Try.

Safe operations

It troubles me that the get operation throws if Try is a Failure. Other than Scala, Java has checked exceptions. That means we can't simply throw the exception of a Failure. Instead it has to be explicitly declared.

By throwing a checked exception nothing is gained, we still need to wrap our code in a try/catch.

// Java
abstract class Try<T> {
    abstract T get() throws Throwable;
}

// we don't need Try at all 😐
try {
    T value = myTry.get();
} catch (Throwable t) {
    ...
}

On the other hand, wrapping the cause of a Failure in a RuntimeException will lead to unsafe code.

// Java
abstract class Try<T> {
    abstract T get() throws RuntimeException;
}

// a RuntimeException is unsafe and hides the cause 😐
T value = myTry.get();

Emmanuel Touzery recently wrote a great blog post about his TypeScript library prelude.ts. It inspired me to think about moving unsafe operations down the type hierarchy.

// TypeScript
type Try<T> = Success<T> | Failure<T>;

class Success<T> {
    get(): T {
        return this.value;
    }
    isSuccess(): this is Success<T> {
        return true;
    }
}

class Failure<T> {
    isSuccess(): this is Success<T> {
        return false;
    }
}

In the above code snippet we used a discriminated union to define the Try type. The get operation is only defined for a Success because a Failure does not wrap a value. Additionally isSuccess uses a type guard this is Success<T> (which is a fancy boolean) in order to give the compiler a hint about the type of this.

// Typescript
if (myTry.isSuccess()) {
    const value = myTry.get();
    ...
}

Now the compiler knows within if that myTry is of type Success and has a method get.

That's really awesome! But how can we achieve something similar in Java? I see only one safe solution that is practical:

// Java
public abstract class Try<T> {
    Try() {
        if (!(this instanceof Success ||
              this instanceof Failure)) {
            throw new Error("Try is sealed");
        }
    }
}

public final class Success<T> extends Try<T> {
    public T get();
}

public final class Failure<T> extends Try<T> {
}

If we use an instanceof check, the Java compiler should be aware of the correct type. The following code is considered to be safe:

// Java
if (myTry instanceof Success) {
    final T value = ((Success<T>) myTry).get();
    ...
}

However, we need an ugly cast — but that is the price we pay when using Java. Btw — this makes the methods isSuccess and isFailure obsolete. We have to remove them in order to enforce the use of the safe variant instanceof.

Handling cases

We already saw above how to pattern-match a Try instance in Scala. In Java we currently have no native pattern-matching at hand and Vavr's Match is part of a different module. But we already have a catamorphism called fold in order to visit both Try cases, Success and Failure:

// Java
final String value = myTry.fold(
    value -> "value: " + value,
    exception -> "exception: " + exception
);

Note: we switched the success/failure lambdas of fold compared to the previous Vavr version. Update: I will rethink this, it plays not well together with other types like Either.

Beside fold there will be several other methods that help us handling the state of a Try or pulling the right value out of it:

// Notes:
// * modulo generic bounds
// * <X extends Throwable>

Try<T> onFailure(Consumer<Throwable>)
Try<T> onFailure(Class<X>, Consumer<X>)
Try<T> onSuccess(Consumer<T>)

T getOrElse(T)
T getOrElse(Supplier<T>)
T getOrElseThrow(Supplier<X>)

Try<T> orElse(Try<T>)
Try<T> orElse(Supplier<Try<T>>)
Try<T> orElseRun(CheckedConsumer<Throwable>)

Try<T> recover(CheckedFunction<Throwable, T>)
Try<T> recover(Class<X>, CheckedFunction<X, T>)
Try<T> recover(Class<X>, T)

Try<T> recoverWith(Function<Throwable, Try<T>>)
Try<T> recoverWith(Class<X>, Function<X, Try<T>>)
Try<T> recoverWith(Class<X>, Try<T>)

More to come soon...

- Daniel

Btw: Did you recognize how nice TypeScript is? This isn't your grandfather's JavaScript ;)