Groovy Functional Programming - Combining Map, Filter and Reduce Operations

In my previous posts, I talked about collect, findAll and inject methods.

Most of the time these methods get used together. Let's take an example. Suppose we want find the sum of squares of even numbers in a list of numbers.

One of the approaches we can take here is to start with inline closures. This means, we want to start with the focus on the big picture - sum of squares of even numbers, without focusing much on how to calculate sum of numbers, square of a number of how to find even numbers.

def numbers = [1, 2, 3, 4, 5, 6, 7]
def evenNumbers = // filter even numbers
def squaresOfNumbers = // calculate squares of numbers in list evenNumbers
def sum = // calculate sum of numbers in squaresOfNumbers  

That's just a pseudo code of that we want to achieve. Let's apply collect, inject and findAll operations.

def numbers = [1, 2, 3, 4, 5, 6, 7]

def evenNumbers = numbers.findAll { it % 2 == 0}
def squaresOfNumbers = evenNumbers.collect { it * it}
def sum = squaresOfNumbers.inject(0) { number1, number2 ->
    number1 + number2
}
println sum // 56

Since finding sum is one of the most common use cases for inject, Groovy readily provides the specialised version of inject called sum. Let's replace inject with sum.

Remember currying?
def numbers = [1, 2, 3, 4, 5, 6, 7]

def evenNumbers = numbers.findAll { it % 2 == 0}
def squaresOfNumbers = evenNumbers.collect { it * it}
def sum = squaresOfNumbers.sum(0)
println sum // 56

Now there are two reasons why you might want to convert the inline closures to named closures.

  1. DRY principle - you want to reuse these closures
  2. Better readability - especially if the closure body spans a few lines, it may hamper the readability of the code.
def numbers = [1, 2, 3, 4, 5, 6, 7]

def isEven = { it % 2 == 0 }
def square = { it * it}
def evenNumbers = numbers.findAll(isEven)
def squaresOfNumbers = evenNumbers.collect(square)
def sum = squaresOfNumbers.sum(0)
println sum // 56
Typically, I would use extract variable refactoring leveraging the IDE support for refactoring. As of writing this post, extract variable for closure works only if the closure is enclosed within parentheses!

At this point, we can also drop the intermediate variables as follows.

def numbers = [1, 2, 3, 4, 5, 6, 7]

def isEven = { it % 2 == 0 }
def square = { it * it }
def sum = numbers
        .findAll(isEven)
        .collect(square)
        .sum(0)
println sum // 56

This can be achieved easily by applying inline refactoring on squaresOfNumbers followed by evenNumbers.

Another approach we could take is starting with the individual operations we write the corresponding closures and then setup the map-filter-reduce pipeline.

You may want to take a look at the slides from my FunctionalConf talk for more examples.

Show Comments