Sunday, July 20, 2008

Exploring Groovy's Optional Typing Features

So I was talking to a friend over a beer after the last BJUG. That night it was the Venkat Show. If you have never had the chance to listen to Venkat talk, all I can say is try harder, the guy is amazing. He opened the JUG with a discussion of interesting features of Java that might have escaped a developers notice (or were forgotten along the way). His second talk was on testing with Groovy. As you will soon see, the testing topic turned out to be helpful with this blog.

Note: code for this blog can be downloaded from here.

After the jug, we started discussing Groovy and dynamic programming in general. As folks who know me well can attest, I am a card-carrying, drank the dynamic cool-aid, Groovy fanboy. So when my friend told me of something he learned at a recent NFJS seminar (he probably brought it up just to rub in the fact that he was there and I wasn't, but I digress), I had to re-think my choice of next gen language. Fear not, the story ends well...

Here is the basic challenge he threw down: "Write a Groovy method that takes a statically typed argument of List. Now call this method from another method, supplying an argument of type Stack. It will work just fine, type mismatch errors."

I didn't believe that this would work. I know Groovy is a dynamic language, but due to its close affinity with Java, I had believed that although optional, when type was provided, it would be honored. So rolling up my sleeves, I wrote some code (See README and code in test1 directory).
class GroovyAdder
{
def doit(List list)
{
println "In list version of doit"

def sum = 0

list.each
{
entry ->

sum += entry
}

return sum
}
}

and then a Groovy test for this class (I told you Venkat's talk would be relevant):
class TestGroovyAdder extends GroovyTestCase
{
def adder

void setUp()
{
adder = new GroovyAdder()
}

void tearDown()
{
adder = null
}

void testList()
{
List list = [0,1,2]
assertEquals 3, adder.doit(list)
}

void testStack()
{
Stack stack = new Stack()
stack.push(3)
stack.push(4)
stack.push(5)
assertEquals 12, adder.doit(stack)
}
}

Running this test, I find that the original statement was true, calling the method with a Stack argument works.
.In list version of doit
.In list version of doit

Time: 0.249

OK (2 tests)

I was so confused! This brings us back tangentially to Venkat's first talk. While the problem is not something he addressed, it does fall into the category of "things forgotten". It took me a while, but eventually, I came around to the realization that Stack implements the List interface. Ah, a Stack IS A List, so all is well. To verify this, I added the following test (See README and code in test2 directory):
void testQueue()
{
Queue queue = new PriorityQueue()
queue.add(6)
queue.add(7)
queue.add(8)
shouldFail(MissingMethodException) { adder.doit(queue) }
}

A Queue does not implement List, so if the typing information was honored, this test should fail, and it did! A MissingMethodException exception was thrown. Problem solved, I sent the solution to my friend. He agreed that this solved the initial problem, but also added that it seemed to be a royal pain that I had to run the program to see the problem, when it could be (and is in Java) detected during compilation. He's right if we are talking about a statically typed language, but Groovy is really a dynamic language at its core. The amount of compile time checking it can do and still adhere to the dynamic mantra is limited. Let's explore this a bit (See README and code in test3 directory).

First, lets just add a second method (and a couple of println's) to the Groovy Adder class:
class GroovyAdder
{
def doit(List list)
{
println "In typed version of doit"

def sum = 0

list.each
{
entry ->

sum += entry
}

return sum
}

def doit(def list)
{
println "In def version of doit"

def sum = 0

list.each
{
entry ->

sum += entry
}

return sum
}
}

We also need to change the test that uses Queue (hint, the call will succeed this time):
void testQueue()
{
Queue queue = new PriorityQueue()
queue.add(6)
queue.add(7)
queue.add(8)
assertEquals 21, adder.doit(queue)
}

Running this test, here is the output we get:
.In list version of doit
.In list version of doit
.In def version of doit

Time: 0.226

OK (3 tests)

This is way cool. Groovy was smart enough to choose the typed version of the polymorphic method when it could and the dynamic version when nothing else could be found.

But, of course, this still could be decided at compile time. Let's try one more thing (See README and code in test4 directory). First, change the uninteresting method we just added as follows (it is now a typed method as well):
def doit(Queue list)
{
println "In queue version of doit"

def sum = 0

list.each
{
entry ->

sum += entry
}

return sum
}

So now we have a Groovy class, where the author provided for the use of Lists (which gives us Stack by default) and Queues. Of course, now I want to use a Set. I'm never happy. So let's add the following test and associated closure:
void testSet()
{
def expando = new ExpandoMetaClass(GroovyAdder)
expando.doit = dynamicDoit
adder.metaClass = expando

Set set = new HashSet()
set.add(9)
set.add(10)
set.add(11)
assertEquals 30, adder.doit(set)
}

Closure dynamicDoit = { Set set ->
println "In set version of doit"
def sum = 0

set.each
{
entry ->

sum += entry
}

return sum
}

We have now crossed the boundary and are firmly entrenched in dynamic land. The compiler can't save us here. Prior to testing with the set, the test code alters the metaclass of the object we intend to test, adding a dynamic method that knows how to handle sets (it could just as easily have handled Iterable or even def and be more general, some folks never learn). Here is the output from running the test now:
.In list version of doit
.In list version of doit
.In queue version of doit
.In set version of doit

Time: 0.23

OK (4 tests)

As stated earlier, the compiler can't help when the code can dynamically change after compilation, and further, if the compiler tried, it would hinder the dynamic features of the language.

So there you go! But wait, there is more. As an added bonus, let's look at what happens when we are using Java (See README and code in test5 directory).

First, we create a simple Adder POJO:
import java.util.List;

public class Adder
{
public int doit(List list)
{
int sum = 0;

for (Object entry: list)
{
sum += (Integer)entry;
}

return sum;
}
}

See the java test code on the download site for an example of how you get a compile time error if you attempt to pass a Queue into this doit method of this class. You can't get there from here, well at least not in Java. So let's see what we can do when using this class in Groovy. Here is the Groovy test code:
class TestJavaAdder extends GroovyTestCase
{
def lister = new Adder()

void testList()
{
List list = [0,1,2]
assertEquals 3, lister.doit(list)
}

void testStack()
{
Stack stack = new Stack()
stack.push(3)
stack.push(4)
stack.push(5)
assertEquals 12, lister.doit(stack)
}

void testQueue()
{
Queue list3 = new PriorityQueue()
list3.add(6)
list3.add(7)
list3.add(8)
shouldFail(MissingMethodException) { lister.doit(list3) }
}

void testQueueWithMop()
{
Adder.metaClass.invokeMethod =
{ String name, args ->

println "In Groovy version of Adder.doit"

if ("doit" == name)
{
def sum = 0

args[0].each
{
entry ->

sum += entry
}

return sum
}
}

Queue list3 = new PriorityQueue()
list3.add(6)
list3.add(7)
list3.add(8)
assertEquals 21, lister.doit(list3)
}
}

Running these tests gives:
.In Java version of Adder.doit
.In Java version of Adder.doit
..In Groovy version of Adder.doit

Time: 0.227

OK (4 tests)

The results from TestList and TestStack are not surprising, as they would work the same way in Java. But notice in testQueue we were able to call the statically typed Java doit method, which requires a List, with a Queue. And we did not receive a compile error! We only found our error at runtime.

The reason, again, is that if Groovy strongly enforced the type expectations at compile time, there would be no way to use the dynamic trick demonstrated in testQueueWithMop (MOP = Meta Object Programming), in which we use another Groovy mechanism to dynamically extend the features of a class, this time a Java class, without the need for source code.

The purpose of this blog was primarily to answer the question of how Groovy's optional type mechanism works and provide my understanding of why things work the way they do. In doing so, I made use of some of Groovy's MOP features. To learn more about these, I refer you to the sources I used. Scott Davis' book, Groovy Recipes, has a great intro to Metaprogramming, along with easy to use code snippets that illustrate the topic. Venkat also has a book, Programming Groovy, that delves even deeper into the topic. I highly recommend both books.

I also did not go into the debate of dynamic versus static typing. Folks a lot smarter than me are discussing this issue. I am principally a spectator to the debate. I will say that for DSLs, which I am very interested in, testing, and script use, the dynamicism of Groovy rocks! And I am tending to lean that direction for other code as well, as long as the code is backed by good unit testing.

Hmmm, guess I picked a side. Well, no one has ever accused me of not having an opinion.

5 comments:

Anonymous said...

BTW, your code can be improved with Groovy's inject method:

def sumAll(collection) {
collection.inject(0) {sum, item -> sum += item }
}

mcjoe said...

Thanks Broady. That is a good point. I should have pointed out that the example of summing a collection is extremely contrived. The real point I was trying to make was how Groovy handled type when it is supplied.

Your solution to summing is definitely more groovy!

mcjoe said...

So testing my examples from work today, I found that test4 does not work with Groovy 1.5.6 but does with 1.6 beta 1. Be forewarned.

Here is the error I got:

1) testSet(TestGroovyAdder)java.lang.IllegalStateException: initialize must be called for meta class of class GroovyAdder(class groovy.lang.ExpandoMetaClass) to complete initialisation process before any invocation or field/property access can be done
at groovy.lang.MetaClassImpl.checkInitalised(MetaClassImpl.java:1239)

...

Greg said...

Is there no way in Groovy to add such a method for _all_ Sets, a la C#'s extension methods or Haskell's instance declarations? That's a shame. :( But it means what you're using isn't really a Set any more, so perhaps you should make a new type (extending from or delegating to the original Set) so that the compiler _can_ help you? Maybe that's not possible, I'm not au fait with Groovy's limitations in this area.

BTW for DSLs, I wouldn't say that Groovy is all that special, it's just that Java is a nightmare. Have you compared any modern statically-typed languages for the purpose?

mcjoe said...

Hi Greg,

Not sure I completely understand the point regarding extension methods. The final example in the blog, where I added a method to the Java class, works equally well with a Groovy class, so you can add the method to all instances of a given class, rather than just an instance.

The reason you can't rely on the compiler to do the checking is that the new type you have created didn't exist at compile time. Rather, it was created at runtime, possibly by a piece of software that didn't even have access to the original source code being modified. That's the nature of dynamic programming (and not a limitation exclusive to Groovy). I enjoy the power that it provides, but it probably isn't for everyone. As I mentioned, testing becomes extremely important, although an argument can be made that it is equally important for programs developed in static languages.

Regarding Java being a nightmare, not sure I agree with you on that one. I do agree it has some quirks, but find a language that doesn't. The reason I like Groovy and the feature that puts it in the "special" category for me, is that it inter-operates so nicely with Java and does a great job of working around some of the quirks. I have been able to easily add some Grooviness to the Java products I work on without incurring any impedance mismatch. Since that is the market I work in, it makes sense to focus in this area. Not dissing other languages, just focusing in the area that make the most business sense for me right now.

Take care.