Groovy Minute - inject() and accumulators

le 12/11/2010 par Cyril Picat
Tags: Software Engineering

What the f*** is a Groovy Minute?

I am thinking about starting a new kind of post about Groovy. Publishing short articles on a regular basis where I will give some Groovy hints and try to exchange on Groovy code with others.

That's what a "Groovy Minute" stands for, some Groovy minutes in your day-to-day work -and mine. Not sure at what pace I will do it it but I am willing to publish something each time I come with some worthy-to-share Groovy code. That's for the title of this series. Concerning the content of these posts, I will talk about some of the language constructs (what they could be used for) and try to share and improve some code I have come up with.

First minute...about the inject() method

So today, I will talk about the inject() method. Why? I have been preparing a Coding Dojo recently and tried to come with a solution to the Roman to numeral problem.

Wikipedia is a good way to grasp the problem and I reproduced part of it below:

# I = 1 # V = 5 # X = 10 etc... # XVI = 1 + 5 + 10 = 16 # XIV = 5 - 1 + 10 = 14

Basically you have to iterate on the string from right to left and sum the number unless it is less than the preceding one (in this case you subtract it).

The best solution (*) I have come with in Groovy is this one:

use(RomanNumberCategory) {
    def reverseString = string.reverse()*.toDecimal()
    def last = 0
    reverseString.inject(0) { sum, current -> 
        sum += sign(last, current) * current; last = current; sum 
    }
}

Why does this work? The closure has access to the local context where it is defined thus is able to read and update the last variable during the inject() recursion.

I have also a one-liner solution for the romanToNumeral function which is less readable but interesting in its usage of inject():

string.reverse()*.toDecimal().inject([sum:0, last:0]) { map, c -> 
    [sum: map.sum + sign(map.last, c) * c, last: c] 
}.sum

If you want to try some code, the code is on the Groovy Web Console with the tests, just play with it! And don't forget to leave a comment here with your own solution (a link to your Groovy Web Console script). I will then update this article with better or interesting solutions to be able to comment and vote for it. Everything is new here (vote widget, web console etc...) so your ideas and remarks are all welcome in the comments too.

Wait a minute... (a bit of thinking)

In which cases inject() is really useful? Inject is the groovy way to do a left fold. Left fold is basically iterating over a list from the head and applying a function to the current element and the accumulated value.

This is very useful to calculate a sum, a factorial, a reverse etc… For a sum, the two following pieces of code are equivalent (**):

list.inject(0) { sum, elt ->
    sum + elt 
}

vs

def sum = 0
list.each { sum += it } 
sum

Yes, it is less functional but it opens you a bunch of possibilities. For example, what happens if you need more than one accumulator? Of course you could inject a map (see one liner above) but I am not sure how efficient it would be. If you prefer to use the pattern just shown, you could rewrite the romanToNumeral() closure as:

def sum, last = 0
string.reverse()*.toDecimal().each { current -> 
    sum += sign(last, current) * current; last = current 
}
sum

So in my mind inject() is a more elegant solution to simple problems, enabling you to write more concise code (without the need to define a variable and to return a value). But keep in mind its equivalent, in case you need for example more than one accumulator.

-- (*) it is still an imperfect solution as it does not handle invalid characters or invalid combinations (eg. 'IC') (**) you should use Groovy sum() in this particular case, of course

Updates: solutions suggested

def romanToNumeral = { string ->

    List.metaClass.injectWithTwoAccu = { accu1, accu2, closure ->
        delegate.each { val ->
            (accu1, accu2) = closure.call(accu1, accu2, val)
        }
        [accu1, accu2]
    }
    
    use(RomanNumberCategory) {
        def reverseString = string.reverse()*.toDecimal()
        reverseString.injectWithTwoAccu(0, 0) { sum, last, current ->
            [sum + sign(last, current) * current, current] }[0]
        }
    }
}

I find this one very elegant but it is a shame you have to write your own injectWithTwoAccu()... Isn't there anything to generalize from injectWithIndex() or injectWithTwoAccu()? A new task for you, Groovy team...