Functional Java in Legacy Code at Rue

Stephen Harrison
Rue Gilt Groupe Tech Blog
4 min readMay 4, 2016

Let’s continue where we left off last time. We’re going to implement a Java version of the sudoku-board validator so we can get a good feel for how Java 8 works. Then we’ll be able to evaluate it as a tool for improving our legacy codebase at Rue. Sometimes we get lucky and can write something new from scratch. Would Java 8 be a natural fit for that?

Java 8 has some functional features, so let’s see if we can use those to make something as pretty as the Scala version from last time. Spoiler alert: We can’t, but not just because of limitations with Java 8’s functional features. Can you guess what’s also missing?

There’s nothing obviously wrong with the structure of the Scala version, so we’ll start with a direct translation and clean it up later.

First, the Index data type. We cheat a little already because we’re assuming direct access to r and c is acceptable.

Scala

case class Index(r: Int, c: Int)

Java 8

static class Index {
private final int r;
private final int c;
public Index(int r, int c) {
this.r = r;
this.c = c;
}
}

Now for the constants that define a completed cell (1–9) and the index of cells in a row, column, or block (0–8).

Scala

Java 8 (Lists)

So things aren’t looking so good. There’s a lot we’re lacking, most notably we’d like easy ways to declare integer ranges. We’ll find a nicer representation when we look at Streams in a bit.

Let’s define some basic functions to return the list of Index entries for each row, column, and block.

Scala

Java 8 (Lists)

At this point we could justifiably ask ourselves why on earth anyone would use Java 8, because it’s making everything so messy with very little obvious benefit. One of the clumsy things we’re doing is converting between List and Stream types in Java 8. There’s a map() method just like in Scala, but it doesn’t work directly on a List.

Let’s go back and make our original definitions more idiomatic using Streams where we can avoid the conversion at every step.

Scala

Java 8 (Streams)

This is starting to feel a bit more native. Each Stream can only be used once, so that’s why we’ve got a Supplier for a new one every time via its get() method. That’s like Iterator in old Java. Let’s rewrite rowOf, columnOf, and blockOf in this style.

Java 8 (Streams)

Now we can use these functions to create the Index structures for each row, column, and block. When we have these, we can concatenate them to make the list of all groups.

Java 8 (Making All Groups)

The rows, columns, and blocks variables look really nice now. Perhaps Java 8 isn’t so bad after all. The allGroups value, however, probably needs some explanation. We’re doing two things: concatenating the Streams with

Stream.of(rows, columns, blocks).flatMap(x -> x)

and converting the nested Streams to nested Lists with

.map(s -> s.collect(toList())).collect(toList())

A feature common to many functional languages is idioms that don’t exactly roll off the tongue but are just the ticket.

While the use of our functions is looking good, the setup of the functions is really messy. We haven’t even started to convert the meat of the validator, the isValid function, and it’s already reaching readability limits. So why is that?

For a start, there are some really awkward interactions between Streams and Lists in Java 8. A significant advantage of Scala over Java 8 is type inference, which allows us to omit type declarations altogether. There are some exceptions, like recursion. But in general we can write code that’s terse without being unreadable. Java needs types, and while we could argue that the body of a particular function in Java 8

r -> indexes.get().map(c -> new Index(r, c));

isn’t much worse than

indexes.map(c => Index(r, c))

in Scala, the mandatory types make things messy.

Let’s leverage arguably the best feature of Java 8, lambdas, to remove some of the cumbersome boilerplate type declarations. We’re going to write our functions in line.

Java 8 (Using Inline Functions)

Compact, dense, a little hard to read, but I think this is about as good as it’s going to get. Let’s revisit our original question, whether we can use Java 8’s functional features to help refactor legacy Java code at Rue more effectively.

It turns out Java 8 has some limitations that make it hard to write clear functional code, even if that code is new. Trying to mimic a language that has a functional paradigm designed in from the beginning did not go that well in our example. Trying to layer Java 8’s features on top of legacy collections classes like List does not go that well either, because there are a lot of places where they simply don’t interoperate smoothly.

At Rue we use Java 8 in legacy code where it improves existing functionality without making it unreadable. In particular, we focus on applications of Streams that allow us to write lambdas in line. We have a bunch of helper methods that hide the explicit conversion of collections to Streams and back again, among other conveniences. And we always rely on good unit tests to make sure we didn’t break anything.

Here’s the complete Java class with those verbose functions collapsed into one-liners. I’d say it fails the test of “provably correct by inspection,” an important requirement of pull requests here at Rue.

Java (One-Liner Anti-Patterns)

We failed because in our effort to get rid of those verbose function type declarations, we made code that’s too hard to understand. So while the total number of statements went down a bit, the readability went down a lot. That’s a bad trade.

As a compromise, we create just enough functions with their irksomely wordy type declarations to make the bodies of other functions more readable. This is the version most likely to pass pull request code review at Rue.

Java (Final Version for Pull Request)

Can you make our final Java 8 code better? Comment with your suggestions if you can. Are you using Java 8 in your legacy Java code? Let us know that too.

Originally published at ruetech.io on May 4, 2016.

--

--