Ashton Kemerling

AK

Java Without If

Over the past year my team has been doing something shocking to a lot of engineers: we’re favoring pure Java over Clojure. We aren’t rewriting all our Clojure code, but we definitely prefer Java for green field projects.

This post is not going to be a compare and contrast between the two, nor am I going to bash Clojure. Language compare and contrast posts always descend into flame wars, and it’s very easy to confuse the result of hard lessons learned with the benefits of a new language.

Instead I’d like to highlight a very strange aspect of our new Java development, and I hope that you’re sitting down for this. Except tests, I have fewer than a dozen if statements currently committed in our Java codebase.

It would be easy to assume that we’re just using Java’s method dispatch to replace if statements; rather than inspecting data and calling if/else on it, you can use interfaces and count on the implementation to provide the difference in behavior. But such an explanation is insufficient: objects don’t magically construct themselves from unstructured data, and Clojure is not without its own dynamic dispatch facilities.

Ultimately the real explanation for this strange code design lies in my colleague’s extremely exceptional Lambda library. It contains a lot of things that a Haskell/Scala developer would recognize such as Either types, function utilities, coproducts, etc. etc.

In particular I’d like to draw your eye to the Either type, which has replaced the vast majority of our explicit if calls. Either is the logical successor to the Java 8 Optional type. Optional represents the presence of a value of type T with Optional::of, or it’s absence with Optional::empty. Either on the other hand is parameterized to two values, L and R, and represents the presence of either a value of type L, or a value of type R.

Why is this a logical extension of Optional? Because while Optional is used to represent a result that may have no value (replacing a null), Either is used to represent a result that might have been a failure, replacing a thrown exception, with convention being that left side values represent failure and right side values representing success.

So, what does that buy us? Well, consider what Optional gets us in the following code snippets.

1
2
3
4
5
6
7
8
String x = Helper.functionOne();
if (x != null) {
  x = Helper.functionTwo(x);
  x = Helper.functionThree(x);
  if (x != null) {
    ...
  }
}
1
2
3
Optional<String> x = Helper.functionOne()
                           .map(Helper::functionTwo)
                           .flatMap(Helper::functionThree);

Optional gives us the ability to say “if a value exists, apply this function to it” repeatedly. It also gives us the ability to chain successive calls that return Optionals together with the flatMap function, eliminating the need for unsightly and error prone manual null checks. It’s also safe for map and flatMap to go from Optional<A> to Optional<B>, which might eliminate the need for intermediate variables in your code.

Either give us much of the same, but with the ability to represent why the computation failed with left values, along with the ability to chain together functions working on an Either type. All of the greatest hits of functional programming are provided for Either, including map, flatMap, and filter.

As a concrete example, imagine a hypothetical JSON parsing library. Parsing is tricky business, you’re all but guaranteed that a failure will happen at runtime. So how do you handle it? Previously you had 4 choices.

  • return null
  • return Optional<ParsedType>
  • checked exception
  • unchecked exception

Null is obviously bad, and unchecked exceptions are also risky. Checked exceptions guarantee that someone will deal with the issue, but they are extremely annoying, and might result in disparate and different exception handlers all over the place. Optional is nice, it’s safer than null and marks in the type signature that failure is an option, but it’s a bit lacking on explaining why a failure occurred.

What if instead this library returned Either<Exception, JsonNode>, or even Either<Set<String>, JsonNode>? The potential for failure is in the type signature again, we don’t need a try/catch, but if we want the unwrapped JsonNode we have to deal with the potential for a left value. And any functions we have that operate only on JsonNode can be passed in using the map function, making chaining a breeze.

Better still we can write other functions that might fail in this form, such as JSON validation, so that we can chain them together using flatMap. If the json parsing has failed, flatMap does nothing (it only works on right values), replacing the need for successive null checks, nested try/catch blocks, or complicated state checking during exception handling to return the correct value.

As a result of all this, you can easily imagine a JSON API endpoint looking something like this:

1
2
3
4
5
6
7
8
public HttpResponse handle(HttpRequest request) {
    return JsonParser.parse(request.getBody())
                     .flatMap(Validator::validate)
                     .map(ServiceObject::businessLogic)
                     .flatMap(JsonGenerator::generate)
                     .match(l -> HttpResponse.internalServerError(l.getMessage()),
                            r -> HttpResponse.ok(l));
}

All the potential failure cases are covered by returning an Either rather than throwing an exception. The very last step is match, which takes two functions to unify a potential left or right value to the exact same type, which in this case is HttpResponse.

What does this get me?

Well, first off I think it’s beautiful. I know that’s a subjective call, but the data flowing neatly from top to bottom without huge nesting if cases and early return values is very aesthetically pleasing to me.

More functionally it’s easier to refactor with the help of the compiler. If I want to add different return status codes to match different scenarios, the compiler helps me out a lot more than if I’m adding an extra return case. If I convert the left side to a HttpResponse early, the compiler will helpfully remind me that the later flatMap calls cannot change Either<HttpResponse, JsonNode> to Either<Exception, BusinessObject>. Such changes are easily fixed once the compiler has pointed it out, but extremely hard to find on your own.

But most fundamentally is that we’ve encoded our code’s states in the type system, not variable states. The potential for JSON parsing to fail is encoded in its type, not in the potential for a variable to null, or false, or for an exception to have been thrown. You’re leaning on the compiler to tell you if you’ve handled the failure cases properly, as the code won’t compile otherwise. Now instead of testing for runtime exceptions you only test to make sure that your business logic is correct.

If you’ve ever been interested in what the Haskell or Scala developers have been talking about with functional type safety, I’d highly recommend taking a look at Lambda to get a taste of it in Java.

Comments