Tuesday, May 27, 2014

Declarative style in Java

The other day at work, we had this problem:

Sort a sequence of rows, based on an input list of columns, where each column might be sorted ascending or descending, and each column might require a transformation first to get to something comparable.

The function interface looks like this:


Here is a concrete example where we sort food items first by their recommended meal (with a transformation that says "breakfast is first, then lunch, then dinner"),  and then by food name in reverse order:


What is the absolutely stupidest way to do this?

The stupidest way to do this is to write your own sort, that happens to order rows by looping through the instruction set. But we don't do it that way. We know very well that sort is hard, and there are library algorithms to do it. Put the ordering logic in a custom Comparator and pass that into sort.



In Java, that looks like

Collections.sort(input, comparator);

This is a declarative style: sort this, to this specification. It separates the algorithm of sorting from the business logic of which row belongs before which other row. A professional Java developer would not consider rewriting sort; they wrap the logic in a Comparator and pass it in.

Heck, even in the C standard libraries, qsort accepts a function pointer. Sort is hard enough that we don't complain about passing in comparison logic, however awkward.

As opposed to filter, which we rewrite every time we need part of a list. If I wanted just the breakfast foods, I might write

List<FoodRow> breakfastFoods = new ArrayList<>();
for (item : allFoods) {
  if(item.getMeal().equals("Breakfast")) {
     breakfastFoods.add(item);
  }
}
return breakfastFoods;

which would be completely imperative, describing the filter algorithm and even allocation and mutation of a new object. Yet this is easy enough that we used to do it all the time. Fortunately Java 8 fixes this:

allFoods.stream()
  .filter(item -> item.getMeal().equals("Breakfast"));

Or in Java 6:

Iterables.filter(allFoods, BREAKFAST_PREDICATE);[1]

Idiomatic Java is moving further and further toward a declarative style, and that's a beautiful thing. While filter may be tremendously simpler than sort, but it's still repetition, and re-implementing it every time is still limiting. The filter method on Stream can perform the operation in parallel, it can weave it in with other stream operations and skip instantiating intermediate lists, it can be lazy and do less filtering -- there are many possibilities when we let go of "how" something works and specify only what it must do.

If your co-workers complain about the new ways of thinking, ask them whether they'd rewrite sort. It's all a matter of degree.

--------
[1] This imports the Guava library and defines a constant:

final Predicate<FoodRow> BREAKFAST_PREDICATE = new Predicate<>() {
   public boolean apply(FoodRow item) {
     return item -> item.getMeal().equals("Breakfast");
   }
}


3 comments:

  1. Note that Guava has the very useful ComparisonChain exactly for this:

    public int compareTo(Foo other) {
    return ComparisonChain.start()
    .compare(meal, other.meal)
    .compare(food, other.food)
    .result();
    }

    ReplyDelete