June 7, 2015

Java 8 lambdas vs. Groovy closures (part 2 of 2)


After taking a closer look at how Java 8 lambdas work and after some practical experience I have to conclude that Groovy’s closures are in every aspect, but most importantly from an “ease of development” point of view, superior to their Java 8 counterpart. Here comes part two of my in-depth assessment. Please check out part 1 here first.

Collection API

Most prominently, predefined functions (closures / lambdas) are provided with the collection API which defines many ways to iterate over a collection.

Stream it, map it and collect it?

As you may have already observed, Groovy closure calls for collections are in general much more concise than their lambda equivalent.

This is because in Groovy, closures are directly integrated in the actual Collection API: closures work on collections, and they return collections. You can thus operate on the actual collection without the need for any “builder pattern” approach to create intermediate objects. The single method call you need is a method named after the desired functionality.

For instance, this code line collects the result of upper casing each element in a list into a returning list:
INPUT_LIST.collect {String myVar -> myVar.toUpperCase()}
Below is the equivalent Java code. As you can see, one has to apply a complex builder sequence because lambdas cannot operate on collections directly and the actual computations are executed only in intermediate builder elements:
INPUT_LIST.stream().map((String it) -> it.toUpperCase()).collect(Collectors.toList());

With or without index

The Java 8 streams API lacks the feature of getting the index of a stream element. Thus there is no Java equivalent to the following concise Groovy code:
INPUT_LIST.eachWithIndex {it, i ->
    OUTPUT_MAP[i.toString()] = it.toUpperCase()
}
return OUTPUT_MAP;
where it is the current iteration element and i is its index in the collection.

Please check out this stackoverflow thread for (rather nasty) workarounds with Java lambdas.

Super-concisely collecting elements

One of the most frequently used iterative functions is creating a new collection by applying a function on every object on a provided original collection; this is the “map” part of the famous map-reduce algorithm. It’s super easy the write it in plain Groovy:
INPUT_LIST.collect {it.toUpperCase()}
In addition to that, Groovy provides an even more concise version known as the special “spread dot operator”. This line of code is equivalent:
INPUT_LIST*.toUpperCase()
The spread dot operator applies a method not to the collection itself, but to every element of the collection, returning a new collection consisting of the method call return values.

With Java lambdas, there is no such thing. You have to stick with the one single version available for mapping:
INPUT_LIST.stream().map(it -> it.toUpperCase()).collect(Collectors.toList());

Collecting maps

Collect / map functionality which involve Map objects work quite different in Groovy and Java. As always, the Groovy version is typically more concise.

When collecting List elements into a Map, the Groovy closure version returns a MapEntry with each function call:
INPUT_LIST.collectEntries {[(it): it.toUpperCase()]}
whereas the Java lambda version uses a dedicated #toMap(…) Collector:
INPUT_LIST.stream().collect(Collectors.toMap(it -> it, it -> it.toUpperCase()));
For the opposite case: when collecting Map elements into a List, there’s a Groovy closure which takes two parameters, the first one is the key and the second one is the value:
INPUT_MAP.collect { key, value -> key + "=" + value }
This is very intuitive and straightforward. Another version which works with a single MapEntry parameter is also available.

That latter version is however the only one available with Java lambdas:
INPUT_MAP.entrySet().stream().map(it -> it.getKey() + "=" + it.getValue()).collect(Collectors.toList());

Easy finding / filtering

Another very frequent operation is finding each element in a collection which matches a given predicate, which could also be perceived as “filtering” functionality.

Again, this is very straightforward in Groovy:
INPUT_LIST.findAll {it == "c"} as List
INPUT_LIST.find {it == "c"}
Note that without as List, the first line would return a Collection object, not a List, even if the original collection is in fact a List! The second version returns the one first matching object. Above code also uses the fact that in Groovy, == is mapped to the #equals(…) method.

The Java version, on the other hand, is slightly more chatty:
INPUT_LIST.stream().filter(it -> it.equals("c")).collect(Collectors.toList());
INPUT_LIST.stream().filter(it -> it.equals("c")).findFirst().get();
Note that the last line would return an Option<T> object if it weren’t for the #get() call.

Sorting and comparing

Of course, both lambdas and closures support sorting and comparing collection elements whereas once again, the Groovy closure versions seem far more concise.

Here’s the Groovy version of sorting a List of Strings according to the first characters of each String in reverse order:
INPUT_LIST.sort(false){(it as String).charAt(0)}.reverse(false)
The two boolean parameters with value false will make sure sorting is executed on a copy of the list, not on the original.

At the moment, #sort(…) breaks type inference, thus it must be casted to String explicitly. This is a known bug.

Intriguingly, Java lambdas also have type inference problems when chaining comparators. For instance, the following line would not compile:
INPUT_LIST.stream().sorted(Comparator.comparing(it -> it.charAt(0)).reversed())
    .collect(Collectors.toList());
as it cannot be inferred as being of type String.

For the rather simple reversing use case though, there luckily is an alternate syntax which works with inference:
INPUT_LIST.stream().sorted(Comparator.comparing(it -> it.charAt(0), Comparator.reverseOrder()))
    .collect(Collectors.toList());

The details are discussed in this stackoverflow thread.

The same problem seems to apply when chaining comparators with #thenComparing(…), but this seems to be an error of the Eclipse compiler only.

Let’s see another example: finding the max value based on a calculation done on each element of the collection. Here’s the Groovy solution:
INPUT_LIST.max { it.charAt(0) }
And here’s the Java lambda counterpart:
INPUT_LIST.stream().collect(Collectors.maxBy(Comparator.comparing(it -> it.charAt(0)))).get();
Note that maxBy returns an Option<T> which you need to read through #get().

Other convenience functions

Both Groovy and Java provide many auxiliary functions on collections which have been implemented with closures or lambdas, respectively. In general, though, the Groovy closure versions are more concise.

Here’s how to flatten a nested collection in Groovy:
INPUT_LIST_NESTED.flatten()
And here’s the same instruction in Java:
INPUT_LIST_NESTED.stream().flatMap(Collection::stream).collect(Collectors.toList());
Here’s how to join collection elements into a String with separator in Groovy:
INPUT_LIST.join(", ")
And here’s the Java equivalent:
INPUT_LIST.stream().collect(Collectors.joining(", "));
Note that in this latest example, the actual functionality happens in the #collect(…) “reducer” method.

Numeric / Range API

Another quite useful application for iteration and thus functional programming is dealing with numeric ranges.

Groovy actually provides a Range collection type which makes the syntax very smooth and natural:
(RANGE_START..RANGE_END_INCLUSIVE).collect()
Java supports ranges as well, but their instantiation is clearly more cumbersome:
IntStream.rangeClosed(RANGE_START, RANGE_END_INCLUSIVE).boxed().collect(Collectors.toList());

Compiler errors

Finally, one last annoyance about Java 8 lambdas I will share here is the error reporting upon compile time error. These messages are typically just horrible, leaving the developer utterly confused.

For instance, what I did here was… wait, can you guess it from the compiler error message? Here’s the code:
IntStream.rangeClosed(RANGE_START, RANGE_END_INCLUSIVE).collect(Collectors.toList());
And that’s the error:
COMPILATION ERROR : 
-------------------------------------------------------------
projects/Java8LambdasVsGroovyClosures/src/main/java/ch/codebulb/java8lambdasvsgroovy/RangeTestsJava.java:[12,-1] 
1. ERROR in projects/Java8LambdasVsGroovyClosures/src/main/java/ch/codebulb/java8lambdasvsgroovy/RangeTestsJava.java (at line 12)
 return IntStream.rangeClosed(RANGE_START, RANGE_END_INCLUSIVE).collect(Collectors.toList());
                                                                ^^^^^^^
The method collect(Supplier<R>, ObjIntConsumer<R>, BiConsumer<R,R>) in the type IntStream is not applicable for the arguments 
    (Collector<Object,capture#1-of ?,List<Object>>)
----------

projects/Java8LambdasVsGroovyClosures/src/main/java/ch/codebulb/java8lambdasvsgroovy/RangeTestsJava.java:[12,-1] 
2. ERROR in projects/Java8LambdasVsGroovyClosures/src/main/java/ch/codebulb/java8lambdasvsgroovy/RangeTestsJava.java (at line 12)
 return IntStream.rangeClosed(RANGE_START, RANGE_END_INCLUSIVE).collect(Collectors.toList());
                                                                        ^^^^^^^^^^^^^^^^^^^
Type mismatch: cannot convert from Collector<Object,?,List<Object>> to Supplier<R>
----------
I forgot to apply the #boxed() method on the range, which would unbox the Range of Integer values to a range of int values.

This is pretty much a typical error message for any lambda compilation error (they’ll usually pop up in your IDE already rather than on the command line, but the problem stays the same). They typically include loads of references to nested generic types which of course is to be expected by the underlying generic abstraction layer of lambdas, but it’s making those error messages very hard to decipher nonetheless. I have to admit that in most cases the error message doesn’t help me at all in finding an error. I don’t care about how complex the underlying mechanisms of lambas are; I expect proper error reporting on the “lambda layer”, without inner classes and generic abstractions leaking through.

I have to admit that Groovy isn’t exactly well known for its excellent error reporting either, mostly due to its highly dynamic nature. Nonetheless, closure errors will in my experience typically either lead to quite readable error messages or Groovy will even run though anyways, with exception and stack trace at runtime (this is why dynamically typed programs are typically developed test-first).

Conclusion

As I wrote in this article’s introduction, I have been looking forward to using the new exciting concept of lambdas in Java mostly because I really got into the apparently similar closure feature in Groovy, hoping to apply the same level of conciseness and readability to Java programs. Well, I was very wrong!

Both concepts share the same basic idea of promoting functional programming thus reducing side effects and facilitate parallel computing which I highly appreciate. And both languages doubtlessly do that very well.

However, there’s a huge difference in language design. Whilst in Groovy, writing iteration logic in closures is actually more readable thus increasing maintainability over e.g. use of nested for-loops, this is in my opinion not true for Java lambdas. Quite the contrary, lambdas are hard to write and quite hard to read with lots of boilerplate code which actually forces us to tradeoff the advantages of functional programming against the negative impact lambda coding style has on maintainability, and this is very bad.

There are some good ideas behind lambdas, but they don’t really matter at the end of the day when you just want to quickly implement some common iteration algorithm. In my opinion, both the grammar as well as the API need some serious revision.

When I do make use of lambdas in real world projects, I do so primarily for two use cases:
  • for specific iteration algorithms such as #find(…) which can actually increase readability when compared with nested conditional for-loops;
  • and to implement the command pattern for runtime pluggable / interchangeable behavior.
I originally intended to write this article for Groovy developers which are interested in the differences to lambdas and as a starting point to map their Groovy closure knowledge to the lambda world which I found quite hard myself. However, I do like to address Java developers as well to simply share my thoughts on the current state of lambdas. If you are a Java developer reading this far, I am keen on hearing whether you share some of my concerns. Feel free to comment below your experience and thoughts on working with Java 8 lambdas.

Again, you may also want to check out the accompanying GitHub project which contains all the code examples presented within the text, some additional lambda / closure examples and JUnit tests which prove equality of each lambda / closure implementation pair.

Update September 6, 2015: Meanwhile, I’ve created LambdaOmega, a small wrapper API for the Java Collection API which is more simple, concise and powerful than its vanilla Java counterpart. It especially fixes many flaws of the lambda API as discussed in this article. Check out the accompanying blog post here or visit its GitHub page. Version 0.1 is now RELEASED!


Pages: 1 2

No comments:

Post a Comment