Types that are groovy, AsType

I was very impressed with Groovy when I first used it such a long time. At that time, I was drawn to its metaprogramming capabilities. More recently, I’ve been pleased by its excellent support for DSLs (like Jenkins pipelines). Lately, it’s Groovy’s approach to types that has really caught my attention.

Discussions about static vs. dynamic typing, and strong vs. weak vs. duck typing, can get confusing. I talked about it for a reason. Groovy’s approach to type checking is quite interesting, and you can find a great explanation at this article. While Groovy has added static type checking (performed at compile time) using the @TypeChecked and @CompileStatic annotations, I’m not as interested in that. What I find really compelling is how its optional typing approach lets us switch between dynamic typing and duck typing when working in a “dynamic mindset.” If you specify types for variables or method/function parameters, Groovy will enforce those types at runtime. However, if you don’t specify a type, it defaults to Object and no type checks are performed. Instead, Groovy’s invocation magic (previously callsite caching, now using invokedynamic in modern versions) provides duck typing behavior. It tries to find a method with the matching name and invoke it – if it works, it works!

Thinking about type systems reminded me of when I discovered structural typing in TypeScript time ago, and I started wondering if Groovy supports structural typing at runtime when using type declarations. In other words, instead of verifying an object’s specific type, it would check if the “shapes” match – meaning the expected type and the actual type have the same methods. The answer, it turns out, is yes The answer is No, (sort of).

To clarify, Groovy doesn’t explicitly define structural typing. However, you can make an object instance implement an interface at runtime using the as coercion operator.

You’ll notice two distinct objects: the source object, a DefaultGreeter instance, doesn’t implement the interface. The other is a Greeter instance that delegates to the coerced object.

This “as operator” (AsType method) creating a new object that delegates calls to the original object… sounds a lot like a Proxy, doesn’t it? I did some more digging, and it is! Groovy dynamically generates a proxy class (and an instance of that proxy) for you. This proxy allows you to pass runtime type checks. It then attempts to invoke the requested method on the original object; if the method isn’t found, you get a runtime error.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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 the code demonstrates, Groovy creates a TextModifier1_groovyProxy class, but not through the typical java.lang.reflect.Proxy mechanism, as evidenced by Proxy.isProxyClass returning false.

I’d like to use this opportunity to highlight another intriguing, relatively new feature related to types in dynamic languages: Python’s type hinting. type hints and mypy. You can add type declarations (type hints) to your Python code, but they don’t impact runtime behavior. The interpreter doesn’t check them, so your code remains dynamically and duck typed. Type hints primarily serve as documentation. However, you can utilize a static type checker like mypy to analyze your source code and simulate compile-time static typing. This is conceptually similar to TypeScript, except you’re not transpiling from one language to another – you’re solely performing type checking.

Licensed under CC BY-NC-SA 4.0
Last updated on Jan 22, 2023 10:49 +0100