Skip to content

Instantly share code, notes, and snippets.

@bdkosher
Last active April 21, 2020 15:06
Show Gist options
  • Save bdkosher/ab8c751ba5082678685558b61557fa95 to your computer and use it in GitHub Desktop.
Save bdkosher/ab8c751ba5082678685558b61557fa95 to your computer and use it in GitHub Desktop.

Calling Collect with Groovy

To be clear, this post is not about using Groovy to make collect calls (Are collect phone calls still a thing? Do people nowadays even know what they are?). No, I am talking about calling methods named collect, which may seem banal but actually warrants attention, especially in light of Groovy 3's long-awaited and much-anticipated release.

Groovy has long provided Closures and extension methods on Iterable and Iterator to implement Java 8 Stream-like capabilities, even when running on Java 7 or earlier.

Using these capabilities to, say, square all even numbers between 1 and 10, would look akin to this:

def evenSquares = (1..10)
                         .findAll { i -> i % 2 == 0 } // take all even numbers
                         .collect { i -> i ** 2 }     // square them
                         .toSet()                     // put the squares into a set

In Java 8, it would look slightly different:

Set<Integer> evenSquares = IntStream.rangeClosed(1, 10)
                                    .filter(i -> i % 2 == 0)      // take all even numbers
                                    .map(i -> i * i)              // square the numbers
                                    .boxed()                      // (re-type from int to Integer)
                                    .collect(Collectors.toSet()); // put the squares into a set

Note that because Groovy now supports Java's lambda syntax, the Java 8 snippet runs seamlessly in Groovy 3. Furthermore, we could rewrite the code to pass Groovy Closures to the Java 8 Stream methods or pass Java lambdas to the Groovy extension methods. Groovy!

Anyways, when comparing the Groovy and Java 8 snippets above, the parallels between the method calls become fairly obvious:

  • Groovy's findAll and Java's filter select specific elements from the stream
  • Groovy's collect and Java's map transform elements
  • Groovy's toSet and Java's collect collect the results into a Set

Wait a sec...there are two collect methods and they're not parallels; one is for mapping and the other for collection. What gives? Did the Groovy JDK authors make a poor naming choice?

The art of naming things is, in my opinion, one of the more difficult aspects of software development. There is a great deal of subjectivity involved as you navigate trade-offs between conciseness and redability, accuracy and conventionality. Take the method filter. Conventionally, this is a proper name. However, I think it is ambiguous, as you could read it as "filter out the elements for which the predicate returns true"--the exact opposite of what the method actually does. In this case, findAll, although less conventional, is arguably more accurate. It finds, and therefore retains, the elements for which the predicate returns true.

Returning to the original Groovy snippet, let us modify the code to collect the squared even numbers into a List instead of a Set.

List<Integer> evenSquares = (1..10)
                                  .findAll { i -> i % 2 == 0 } // take all even numbers
                                  .collect { i -> i ** 2 }     // square them and put them into a List

Notice that I did not need to replace toSet() with toList(). This is because the collect method returns a java.util.List already. Here's how the method is defined on java.util.Iterable:

public List collect(Closure transform) Iterates through this Iterable transforming each entry into a new value using the transform closure returning a list of transformed values.

assert [2,4,6] == [1,2,3].collect { it * 2 } Parameters: transform - the closure used to transform each item of the collection Returns: a List of the transformed values

The Stream API introduced a new data type called Stream.

Dual Collects

So here we have

Conclusion

Naming things is hard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment