Computation: “There is no try”

November 09, 2010


Often a complex, multi-step calculation requires your code to react to many circumstances. Typically you use exception handling to separate the normal control flow from error handling:

Option<BigDecimal> result = Option.none();

try {
  result = compute();
} catch (WhateverException e) {
  // Perhaps rethrow.
}

if (result.isEmpty()) {
  // Perhaps retry, or set a dummy value, or some other fancy stuff.
}

Here we’re using an Option to encapsulate an optional value.

Exception handling code like this not only gets complicated but becomes really hard to read. Imagine a three or four (or more!) step process where each step can fail:

Step1 step1 = null;
try { 
  step1 = step1(); 
} catch (Step1Exception e) {
  // do stuff, or maybe not
}

Step2 step2 = null;
try { 
  step2 = step2(step1);  // needs step1
} catch (Step2Exception e) {
  // do stuff, or maybe not
} 

// more steps

Now imagine that some of these operations may throw a RuntimeException, which you need to handle because sub-step 4a needs to, and you can see the syntactical (is that a word?) pain piling up. You can’t escape the pain, but you can manage it a bit.

Let’s introduce a new type, Computation, that makes code like this more fluid, type-safe, and readable. We’ll rewrite the first example to rethrow the exception if we got one, and supply a dummy value if there’s no actual result:

Computation<BigDecimal> computation = ... // described below
BigDecimal result = computation.throwIfFailure().getOrElse(BigDecimal.ZERO);

Here’s the signature for a Computation:

public abstract class Computation<T> implements Iterable<T> {
  // Boring methods:
  public abstract boolean isFailure();
  public abstract boolean isSuccess();
  public abstract Exception getFailure(); // Null if success (maybe make this return Option<Exception>?)

  // Sweet, fluid methods:
  public abstract Option<T> toOption();   // Option.none() if a failure
  public abstract Option<T> throwIfFailure() throws Exception; // Like toOption(), but throws if a failure
}

Just like Option, Computation has two concrete subtypes: Result and Failure. The implementation of the abstract Computation methods in the subtypes is straightforward. (Bonus: Why is Computation an Iterable? So you can use a nice Java syntax hack and put your Computation in a for loop which will only run the block if the Computation is a Result.)

Some nice factory methods for Computation do the dirty work of getting rid of the try/catch blocks in your code:

// Gives you a Result if no Exception, Failure otherwise
Computation<Integer> computation = Computation.compute(new Callable<Integer>() {
  @Override
  public Integer call() throws Exception {
    // do stuff
  }
});

// Adapt a Future<T> into a Computation<T>.
ExecutorService executor = ...
Computation<Integer> async = Computation.compute(executor.submit(...));

After getting your code into shape like this, you can next separate your computations into clean, isolated, composable Callable implementations which make testing much simpler.

Note that Computation is just like a Future but presents an alternative, perhaps smoother interface. Computation is also very similar to Scalaz’s Validation and Lift’s Box, but without monadic niceness of Scala.