Monday 6 September 2021

Groovy Types, AsType

It was such a long time ago that I expressed my profound admiration for Groovy. At that time I was impressed by its metaprogramming capabilites, recently I've been delighted by what a beautiful platform for DSL's it provides (Jenkins pipelines...), and lately it's its approach to Types that has caught my attention.

The dynamic vs static typing, and strong vs weak vs duck typing debate can be a bit confusing. I talked about it long in the past. Groovy's approach to type checking is pretty interesting, and this article is an excellent reference. In the last years it has gained support for static type checking (checks done at compile time) by means of the @TypeChecked and @CompileStatic annotation, but I'm not much interested on it. What's really appealing to me is that when working in the "dynamic mindset", its Optional typing approach allows us to move between dynamic typing and duck typing. When you declare types for variables or method/function parameters, Groovy will do that type check a runtime, but if you don't declare the type, it will be declared as Object, but no type checks will be applied, and the Groovy invokation magic (callsite caching in the past, invokedynamic in modern versions) will give you a duck typing behaviour. It will try to find a method with the given name, will try to invoke it, and if it works, it works :-)

All this thinking about type systems reminded me of my discovery of structural typing via TypeScrit time ago, and made me wonder if when using type declarations Groovy would support structural typing at runtime (I mean, rather than checking if an object has an specific type, checking if the "shapes" match, that is, the expected type and the real type has the same methods). The answer is No, (but a bit).

In other words, Groovy does not define structural typing. It is however possible to make an instance of an object implement an interface at runtime, using the as coercion operator.

You can see that there are two distinct objects: one is the source object, a DefaultGreeter instance, which does not implement the interface. The other is an instance of Greeter that delegates to the coerced object.

This thing of the "as operator" (AsType method) and the creation of a new object that delegates calls to the original object... sounds like a Proxy, n'est ce pas? So I've been investigating a bit more, and yes, Groovy dynamically creates a proxy class for you (and an instance of that proxy). Thanks to the proxy you'll go through the runtime type checks, and then it will try to invoke the requested method in the original object, and if it can not find it you'll get a runtime error.


import java.lang.reflect.*;

interface Formatter{
    String applySimpleFormat(String txt);
    String applyDoubleFormat(String txt);
}

class SimpleFormatter implements Formatter {
    private String wrap1;
    private String wrap2;
    
    SimpleFormatter(String w1, String w2){
        this.wrap1 = w1;
        this.wrap2 = w2;
    }

    String applySimpleFormat(String txt){
        return wrap1 + txt + wrap1;
    }

    String applyDoubleFormat(String txt){
        return wrap2 + wrap1 + txt + wrap1 + wrap2;
    }
}

class TextModifier{
    String applySimpleFormat(String txt){
        return "---" + txt + "---";
    }
}

def void formatAndPrint(String txt, Formatter formatter){
    System.out.println(formatter.applySimpleFormat(txt));
}

def formatter1 = new SimpleFormatter("+", "*");
formatAndPrint("bonjour", formatter1);

def modifier = new TextModifier();
try{
    formatAndPrint("bonjour", modifier); //exception
}
catch (ex){
    //runtime exception, when invoking the function it checks if modifier is an instance of Formatter
     println("- Exception 1: " + ex.getMessage());
}    

Formatter formatter2;
try{
    formatter2 = modifier;
}
catch (ex){
    //runtime exception, it tries to do a cast
// Cannot cast object 'TextModifier@1fdf1c5' with class 'TextModifier' to class 'Formatter'
    println("- Exception 2: " + ex.getMessage());
}   


formatter2 = modifier.asType(Formatter); //same as: modifier as Formatter;

//this works fine thanks to the Proxy created by asType
formatAndPrint("bonjour", formatter2); 

println "formatter2.class: " + formatter2.getClass().name;
//TextModifier1_groovyProxy

//but it is not a java.lang.reflect.Proxy
println ("isProxyClass: " + Proxy.isProxyClass(formatter2.class));


As you can see in the code above, Groovy creates a "TextModifier1_groovyProxy" class, but it does so through a mechanism other than java.lang.reflect.Proxy, as the Proxy.isProxyClass check returns false.

I'll leverage this post to mention another interesting, recent feature regarding types in a dynamic language. Python type hints and mypy. You can add type declarations to your python code (type hints), but they have no effect at runtime, nothing is checked by the interpreter, your code remains as dynamic and duck typed as always, the type hints work just as a sort of documentation. However, you can run a static type checker like mypy on your source code to simulate compile time static typing. This is in a sense similar to what you do with TypeScript, only that you are not transpiling from one language (TypeScript) to another (javascript), you are only doing the type checking part.

No comments:

Post a Comment