topr
topr

Reputation: 4612

Groovy closure-on-collection-methods inconsistent type transformation

There is strange behaviour in Groovy. Have a look on two examples below:

def list = [[BigDecimal.ONE]]
list.each {
    println it.class
}

prints:

class java.util.ArrayList

and

def list = [[BigDecimal.ONE]]
list.each { BigDecimal it ->
    println it.class
}

prints:

class java.math.BigDecimal

The only difference in the examples is that the 2nd one has a argument type for the closure specified. But this doesn't explain why and how the inner List is being transformed to BigDecimal. I would rather expect ClassCastException. Moreover this behaviour is inconsistent, as if there are more elements in the inner list it fails with MissingMethodException.

We've found that this magic type conversion happens in ClosureMetaClass (line: 256)

Is it a designed behaviour or a bug?

EDIT: I came across the above issue while trying to stub a method with Spock. The method takes Collection as a parameter. Consider another example:

def 'stub a method with collection as argument'() {
    given:
    def input = [1, 2, 3]
    def capturedArgument
    List listStub = Stub()
    listStub.addAll(input) >> {
        capturedArgument = it
    }

    when:
    listStub.addAll(input)

    then:
    input.class == capturedArgument.class
}

It fails with:

Condition not satisfied:

input.class == capturedArgument.class
|     |     |  |                |
|     |     |  [[1, 2, 3]]      class java.util.Arrays$ArrayList
|     |     false
|     class java.util.ArrayList
[1, 2, 3]

The problem is that argument it comes as List embedded in another List into the method stubbing closure. WTF?

The only way to overcome this is stubbing method with the exact same argument type as input type like

listStub.addAll(input) >> { ArrayList it ->

...then the test passes. It's a real no-go as I need to use interface as the stub argument type, not a specific implementation. And when it's declared like

listStub.addAll(input) >> { List it ->

or

listStub.addAll(input) >> { Collection it ->

...it fails the same way as without type because input list gets embedded in another list.

Here's live example if you like to run and play with it

Upvotes: 4

Views: 803

Answers (1)

cfrick
cfrick

Reputation: 37033

groovy destructures the items provided to the closure (best example is each on Map, where key and value are passed). so it is consistent on consistent use:

[[BigDecimal.ONE],[BigDecimal.ONE]].each{ BigDecimal it -> println it } 
//=> 1
//=> 1
[[BigDecimal.ONE, BigDecimal.ONE]].each{ a, b -> println "$a and $b" }
//=> 1 and 1

Upvotes: 2

Related Questions