“Groovy cast” tries too hard

2013-12-22 in groovy java
With sufficient force, any peg will fit through any hole.

Groovy has an entire three different types of type-casts that can occur. The first two are inherited from Java, albeit with substantially more permissive semantics.

  • Implicit casting occurs when a type other than the one expected occurs in a particular context. Numeric types may be coërced to each other or to strings, and objects may be up- or down-cast; Java only permits widdening coërsions and up-casting in implicit casts, but Groovy allows both directions due to its dynamic type system. For example, in the expression 2+3.14, the value 2 is implicitly coërced from int to double (or BigDecimal in the case of Groovy) in order for the types to match, resulting in a final value of 5.14.

  • C-style casting uses a notation borrowed from C, in which the desired type for an expression is placed in parantheses before the expression to cast. This has similar semantics to implicit casts, except that the type is selected by the programmer, and the cast will occur even if the compiler would not otherwise think it necessary. In Java, explicit casts are the only way to get narrowing coërsions and down-casts. In 2+(int)3.14, the value 3.14 is implicitly narrowed to an int, so the result of the whole expression is 5.

Groovy’s third type of cast, usually called the Groovy cast, follows the expression to cast, and is formed by placing the desired type after the keyword as. Most programmers believe it to be merely an alternate syntax to C-style, and some prefer the syntax, even though it violates the inherent right-to-left flow of information which normally characterises expressions in the C family. There’s also a number of other oddities involving this syntax. The Groovy cast operator has surprisingly high precedence — 3/2 as double yields 1.5 (ie, it is parsed as 3/(2 as double)); (int)1.5 as String throws a GroovyCastException because the String conversion occurs first. 3/2 as double < 1 is a syntax error, since the < looks like a generic parameter.

In actuallity, the Groovy cast has semantics distinct from that of the C-style cast. A Groovy cast will attempt to perform a number of object-to-object coërsions (as opposed to strict casts). Some of these are actually useful: [1,2,3] as Set creates a set of Integers concisely, working around Groovy’s lack of a built-in set literal syntax. On the other hand, most of these conversions are ill-defined. For example, casting a value with unknown type to a List results in a List containing that one value, unless that value has a separate conversion to List, or is null.

One of the most bizarre and surprising of these conversions, however, is that any object may be cast to any interface — it doesn’t need to implement the interface, or even have any of its methods. Granted, Java interfaces are a bit problematic, mainly owing to Java’s ban on default interface implementations, and the complete obliviousness of many interface designers on what an interface exactly is (see the remove method on interface Iterator). But what Groovy does is downright ridiculous.

To demonstrate, consider the below program, which you can run yourself.

Main.java

 1package gl.lin;
 2
 3import java.util.Iterator;
 4
 5public class Main {
 6  public static void main(String args[]) {
 7    Iterator<Object> it = GetIterator.getInstance();
 8    System.out.println("Got iterator: " + it);
 9
10    while (it.hasNext())
11      System.out.println(it.next());
12  }
13}

GetIterator.groovy

1package gl.lin;
2
3class GetIterator {
4  static Iterator<Object> getInstance() {
5    return new Object() as Iterator; // !
6  }
7}

The Java code simply asks the Groovy code for an Iterator, then does something completely reasonable by assuming that what it got really was an Iterator. The Groovy code, however, just creates a raw Object (which has none of Iterator’s methods) and Groovy-casts it to an Iterator. Here’s what happens when we execute this program:

 1$ ./gradlew run
 2
 3...
 4
 5Got iterator: Object_delegateProxy@3f69582a
 6Exception in thread "main" groovy.lang.MissingMethodException: No signature of m
 7ethod: java.lang.Object.hasNext() is applicable for argument types: () values: [
 8]
 9Possible solutions: inspect(), hashCode(), wait(), collect()
10	at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.unwrap(ScriptByteco
11deAdapter.java:55)
12	at org.codehaus.groovy.runtime.InvokerHelper$invokeMethod.call(Unknown S
13ource)
14	at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSi
15teArray.java:42)
16	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCa
17llSite.java:108)
18	at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCa
19llSite.java:124)
20	at Object_delegateProxy.hasNext(Script1.groovy:19)
21	at gl.lin.Main.main(Main.java:10)

As can be seen in the output, the value returned by Groovy was an Iterator, as far as the type system is concerned. Yet the moment it calls one of the Iterator methods, an exception is thrown to the Java code, since there is no method Object.hasNext().

The particular exception type being thrown is also of interest. Since Java has so many interfaces like Iterator where some part (like remove()) ends up becomming considered “optional” — because it really should be part of a different (sub)interface — convention is to throw an UnsupportedOperationException from methods that a particular implementation of an interface does not support. Groovy instead throws a MissingMethodException, which isn’t even part of the Java standard libraries, but instead within Groovy.

This example is contrived, since the Groovy code pretty much says it will do one thing and then immediately does something different. However, in larger code-bases, such things do occur on accident. Groovy’s pathological sometimes-dynamic-sometimes-weak-pretends-to-be-static type system leads many programmers to insert numerous paranoid explicit casts; those who favour Groovy casts wind up performing nonsensical “conversions” that nobody thought possible, rather than asserting that values had particular types.

All in all, the Groovy cast is almost completely a non-feature:

  • It is hard to read. The syntax has counter-intuitive precedence, and violates the language’s direction of flow of information as well as the end-weight theorem. Especially with shorter type names, a Groovy cast at the end of an expression is easily missed.

  • It is largely redundant with the C-style cast, which is generally easier to read except within the most simple of expressions. Those few useful additional conversions performed by the Groovy cast (such as ListSet) should really be provided by Groovy’s implementation of the C-style cast.

  • The vast majority of “conversions” provided by the Groovy cast are useless, have varying semantics depending on the run-time value being cast, or undermine the Java interface system. Values which have passed through a Groovy cast cannot be expected to have any particular semantics or methods, even if the resulting static type of the value would suggest otherwise.