When Duck Typing Bites You on the Ass

I've become quite familiar with Jenkins at work recently. Jenkins uses Groovy, and Groovy is - according to Wikipedia - "a Java-syntax-compatible object-oriented programming language for the Java platform." I would call it a superset of Java, as any Java statement works but it adds a bunch more weird and interesting stuff on top of Java. My favourite Groovyism is if ( variable ) { /* do something */ } - this means "if variable is defined, do something."

The language I use most is Bash, although I've written bad code in dozens of languages. I've written a lot of Java at work, and I've grown to hate the language for its verbosity and its strict data typing. So I was pleased to find Groovy is both less verbose and a lot less strict about data typing (more like Bash): we're definitely talking ducks. If you look at example Jenkins code, you'll see an immense number of def variable statements, which defines a variable without specifying a type. And I was very happy about that right up until it turned around and bit me on the ass.

I was bringing in a number from a shell script - specifically, an SVN revision number. Since I needed a range of SVN revisions and I didn't want the range to include the bottom end of the range, I incremented the variable with svnRevision++. Worked great. Mostly. But it failed periodically. One day I dug in, and noticed that it was failing because I had a revision number of '25:', and the previous revision, the one that was incremented, was '259'. (The problem would have been somewhat clearer if I'd used the more verbose form svnRevision = svnRevision + 1 because then the resulting number would have been 2591 with the Integer '1' being coerced to a String and appended ...)

Those familiar with their ASCII tables will realize that ':' follows '9' on the ASCII table. I was incrementing a String value, and nine times out of ten this did the right thing (or close enough). And the tenth time, it bit me on the ass - hard.

class DuckType {
    static void main(String[] args) {
        println("String:");
        def svnRevision = "4";
        def i = 0;
        println("svnRevision is ${svnRevision}");
        while ( i < 6 ) {
            svnRevision++; i++;
            println("svnRevision is ${svnRevision}");
        }

        println("Integer:");
        Integer svnRev = 4;
        i = 0;
        while ( i < 6 ) {
            svnRev++; i++;
            println("svnRev is ${svnRev}");
        }
    }
}

The output looks like this:

String:
svnRevision is 4
svnRevision is 5
svnRevision is 6
svnRevision is 7
svnRevision is 8
svnRevision is 9
svnRevision is :
Integer:
svnRev is 5
svnRev is 6
svnRev is 7
svnRev is 8
svnRev is 9
svnRev is 10

In this case I'm creating an Integer-as-String by calling def svnRevision = "4" - the number is in quotes. In my original use case it was even less obvious: I had a shell script write the SVN revision number to a file, and then had Groovy read in the file. If I'd stopped and thought about it, I would have realized file reads are always strings, but it looked like an Integer and I treated it like one - which worked nine times out of ten.

To fix this problem you'll want svnRevision.getClass() which will tell you in a debugging println() that svnRevision is in fact a String, and svnRevision.toInteger() which will output the contents of svnRevision as an Integer.

Let's make this really clear:

def a = "9";
println("variable a is ${a}");
println("variable a is " + a.getClass());
a++;
println("variable a after ++ is ${a}");

def b = "9";
println("variable b is ${b} " + b.getClass());
b = b.toInteger();
println("variable b is ${b} " + b.getClass());
b++;
println("variable b after ++ is ${b}");

This gets the following output:

variable a is 9
variable a is class java.lang.String
variable a after ++ is :
variable b is 9 class java.lang.String
variable b is 9 class java.lang.Integer
variable b after ++ is 10

Since Groovy supports standard Java typing, I've since reverted to using fairly strict typing and apologised to Java in my heart for bashing it about that strict typing (if not for its verbosity).