Friday, June 12, 2015

Expression-oriented programming in Groovy: transpose() is zip()

The term "expression-oriented programming", as mentioned in these blog posts, resonated with me:

Groovy is by no means a purely functional language, but it does include a lot of the basics. Moreover, it includes a few other goodies that make it really nice for expression-oriented programming.

Here's a set of posts about some of these constructs.

One oddity about Groovy's support for functional programming is that Groovy chooses unusual names for some common functions from functional programming.

I believe this is because Groovy chose names from its object-oriented heritage (SmallTalk), rather than from the functional-programming canon.

For example:

  • map() is called collect() in Groovy
  • fold() or reduce() are inject() in Groovy
  • filter() is findAll() in Groovy

One of the most obscurely-named such methods in Groovy is transpose(), and as a result it's easily overlooked.

A very useful function in functional programming languages such as Haskell and Scala is zip(), which is used to combine corresponding elements from more than one collection.

Suppose we have two lists, a and b, containing numbers. And we want to find the maximum from each pair of corresponding numbers from these lists.

Groovy provides a lot of nice methods for working with a single list. But faced with two lists to be traversed together, many would revert to old Java-style code:

def a = [5, 10, 15, 20, 25]
def b = [20, 16, 12, 8, 4]

def r1 = []
for (i in 0..<a.size()) {
  r1[i] = Math.max(a[i], b[i])
}

assert r1 == [20, 16, 15, 20, 25]

We could try to use one of Groovy's iteration methods, eachWithIndex(), but the result is hardly better:

def r2 = []
a.eachWithIndex {v, i ->
  r2[i] = Math.max(v, b[i])
}

assert r2 == [20, 16, 15, 20, 25]

The answer is to use transpose():

def r3 = [a, b].transpose().collect {v, w -> Math.max(v, w)}

assert r3 == [20, 16, 15, 20, 25]

It's a little tricky until you get the hang of it: transpose is called on a list of lists, and it returns a new list of lists. Each list in the new list contains all of the elements at the same position in the original lists.

Actually, for built-in methods like max() that Groovy defines on collections, we can use the spread operator:

def r4 = [a, b].transpose()*.max()

assert r4 == [20, 16, 15, 20, 25]

Suppose instead of taking the max of two items we wanted the sum:

def r5 = [a, b].transpose()*.sum()

assert r5 == [25, 26, 27, 28, 29]

transpose() is also nice because it generalizes nicely beyond the case of just two lists.

def c = [3, 6, 9, 12, 15]

def r6 = [a, b, c].transpose()*.sum()

assert r6 == [28, 32, 36, 40, 44]

Of course, our lists do not have to be of the same type.

Here's an example where one list contains strings and another lengths, and we want to pad each string to the corresponding length:

def strings = ["one", "two", "three", "four", "five", "six"]
def lens = [1, 2, 3, 4, 5, 6]
def r7 = [strings, lens].transpose().collect {item, len -> item.padRight(len)}

assert r7 == ["one", "two", "three", "four", "five ", "six   "]

Java 8 added lambdas and the Streams API, which permit many functional idioms. A zip() method was originally included in the Java 8 SDK previews, but unfortunately was removed before release. Never mind, we have it in Groovy!

So despite its unconventional name, keep transpose() in mind when working with lists. And if you have any interesting usages yourself, post them (or links) in the comments!

No comments: