Tuesday, December 14, 2010

Groovy Puzzler

Consider the following code snippet:


def A = "A"
def B = "B"

def map = ["${A}":1, (B):2, C:3]

println map
println map[A]
println map[B]
println map["C"]
println map[C]


What will be output?

The first thing to consider is, what will groovy think the map is, given the 3 different ways entry are created. However, printing it will return:


[A:1, B:2, C:3]


Looks like it all ended up being the same thing, no?

Now consider what attempting to print a, b and c will give you:


null
2
3
Exception thrown

groovy.lang.MissingPropertyException: No such property: C for class: ConsoleScript27
at ConsoleScript27.run(ConsoleScript27:10)


Now, let's think about what we've done here. With A, we've added it using the normal syntax for dereferencing an object inside of a string, by surround it with ${} inside the string itself, somewhat similar to JSTL. This is returning the string 'A' correctly, but when attempting to reference it, it's not found, thus returning null.

With B, the correct way to dereference and object was used, by surrounding it with a parens. This works as expected, and the referring object can even be used as a key.

C used the string property approach. That is, groovy treated it as the literal string 'C'. This is why referencing it as a ["C"] worked and [C] caused an exception.

The following groovy page contains all you need to know about map manipulation in groovy: http://groovy.codehaus.org/JN1035-Maps

Next, let's consider some of the very useful operations that groovy adds to normal java collections. In this case, max(). What will the following return?
def someList = ["75", 32, 3.2]

println someList.max {item ->
return null
}
The answer is "75". This is because it's the first item in the list. If the closure passed to the max method returns null, the first item in the collection will be returned.

Now, imagine the following scenario. You mess up how you reference the map, inadvertently causing null to be returned for all calls. You then use this map in a closure for the max method. You don't catch any of this because in your unit test the first element should be the max. Now, imagine the list you're operating on was obtained from a database via GORM, which means the order will vary from call to call, since there is no order guarantee without an orderby clause, which isn't there. So, on the webpage you'll see the value change everytime you call it, but when you debug, the map will look correct. Hours later, you'll discover that you didn't create the Map correctly, even though inspecting it in debug, and printing it out in unit testing will show that it looks correct.

While I don't know Groovy that well yet, I can understand at least some of the mechanisms that would lead to that behavior. However, I wonder two things:
  1. Why does the map print out the way it does? I need to do some more digging into the Groovy Map implementation to figure out what underlying mechanism is causing it, but it seems like there could be some kind of way of determining how it's being stored. Meaning, there's obvious differences between the 3 entries, but you won't see it visually when printing, and you won't be able to determine it easily in a debugger. (Although, I may have missed something important when looking)
  2. Should the closure returning null for every element behave this way? I suppose there isn't much way around it, when you consider a scenario where null is returned in some but not all cases.




3 comments:

Danno Ferrin (aka shemnon) said...

regarding the maps:

The first issue is that the string "${A}" will result in a groovy.lang.GString, not a String.

The second issue has to do with the unbound reference to C. Since it looks like a variable it will be treated as a variable. And since it is not declared it is an 'unbound variable' that will be looked up in the binding. The script could work if called from some other source and passed in with a binding where C is defined to some value.

Lucas Ward said...

@Danno

Thanks for the clarification. I assumed it was probably something related to GString. It reminded me of a classloader issue where I thought I had the same two classes, but it turns out they were loaded from different classloaders. I've seen quite a few bugs related to confusion around String and GString when googling for answers. I'll have to make sure I do more research on it.

I understand the issue underlying your second point. When within [] tags it assumes it's a string, kind of like a ruby hash, but outside of that it considers it to be a variable, which it isn't finding. It can still be an easy mistake to make, since only position relative to brackets indicate usage. It does throw an exception when hit though, so its something I would likely catch in a unit test. It still can be confusing if you're not used to looking at Groovy code.

Is there some easy way of seeing the difference between a GString and a String at runtime? Or is it just familiarity with Groovy over time?

Wombatolog said...

I try to println the element of this map by first key (GString = "A"), result is null again.

groovy:000> println map[map.keySet().iterator().next()]
null
===> null

Seems like a bug in the groovy map.