Groovy dynamic typing

By dynamic languages, types are inferred at run time, and methods and arguments are checked at run time. This way, we can inject behavior into the class at run time, making the code much more extensible than strictly static typing. In short, with dynamic typing, you can achieve a more flexible design with less code than Java.

3.1 Type checking in Java

Java is a strongly typed static language, so detailed that you don’t miss a semicolon. Freedom has been sacrificed for maximum security.” Most of the time, the Java compiler acts like a long-winded old woman. In the code below, the compiler still requires the code to perform an explicit conversion here, although we are sure that the mechanism of super.clone() means that it must return User_.

class User_ implements Cloneable {
    public String name;
    public Address_ address;

    @Override
    protected User_ clone(a) throws CloneNotSupportedException {
        return (User_) super.clone();
    }

	// omit the constructor
	/ / omit the toString
}

class Address_ implements Cloneable{
    public String street;
    public String houseNumber;

    @Override
    protected Address_ clone(a) throws CloneNotSupportedException {
        return (Address_) super.clone();
    }
	// omit the constructor
	/ / omit the toString
}
Copy the code

When call cloning method, the compiler always insist that we deal with the abnormal tested (unless you give it to the superior), although we can ensure the CloneNotSupportedException won’t happen.

try{
    User_ clone = user_.clone();
    System.out.println(clone.toString());
}catch (CloneNotSupportedException ex){
    ex.printStackTrace();
}
Copy the code

3.2 What does dynamic typing bring

Operations that the Java compiler might consider “outrageous” (such as accessing a name field of type Object, or calling a method that doesn’t exist on Object itself) can be interpreted correctly at runtime for languages that support dynamic typing. Then there seems to be nothing wrong with this operation.

The compiler abandons interception for these “shouldn’t exist” errors — it assumes that at runtime these problems will be resolved with dynamic validation or injection. The advantage is that when this premise is true, we can “program for intuition”, and we don’t have to add explicit casts over and over again to please the compiler.

Of course, this is not to say that dynamic typing is everything. Leaving aside the risks and costs of dynamic typing, a smart and thoughtful programming language that anyone would want to use, such as Python next door. Using Groovy to implement the first example, we find that the code is much simpler:

import groovy.transform.Canonical

@Canonical
class User implements Cloneable {
    String name
    Address address

    @Override
    def clone() throws CloneNotSupportedException {
        super.clone()
    }
}

@Canonical
class Address implements Cloneable {
    String street
    String houseNumber

    @Override
    def clone() throws CloneNotSupportedException {
         super.clone()
    }
}

user1 = new User(name:"Wang Fang".address: new Address(street:"LA".houseNumber: "132-052"))
user2 = user1.clone()

// The compiler does not report errors and can report the correct results at runtime
// The only downside is that the IDE may not give reasonable code hints.
println user2.name
println user2.address.street

This method verifies that user2 and user1 are not the same reference.
println user2.is(user1)

// Note that Groovy does not evaluate HashCode in exactly the same way as Object.
// So if you compare user1.hashcode () == user2.hashcode (), you get true.
// If you want to compare both references in Groovy according to traditional hashCode(), you need to have the User actively call the super.hashcode () method from Object.
Copy the code

First of all, Groovy doesn’t force exceptions, so we saved a try-catch first. Second, according to the clone() method declaration, user2 is originally an Object, but we all know that it must be of type User. So even if you access user2’s name and address properties directly without any cast, the Groovy compiler doesn’t complain much.

It’s important to note that Groovy, like Java, is a strong language type. The current consensus is whether a program should try to cast as many types of incompatibility problems as possible, or simply give an error. If the latter, the language is strongly typed 0. In fact, both Groovy and Java do this. The difference between the two is that Java early in compile time notes inconvertible types: can not cast ‘XXX’ to ‘XXX’. Forgiving Groovy first assumes that what we’re doing is reasonable, and then postpones this check until runtime.

If you divide programming languages into static, non-static, strongly typed, and weakly typed, you can actually divide them into four categories. Here’s a picture from the Internet:

It’s important to point out the jewel in the JVM language family: Scala. The Italian job “is its specialty, the programmer can custom some implicit conversion function first, and then based on these rules of context for happily” weak “programming, then scalac at compile time will secretly call transformation function to maintain compatibility (in short, the compiler and at least one party to do some work). Scala, then, is not out of the realm of statically strongly typed languages.

As an aside, there’s a lot of debate about whether Python is strongly typed or weakly typed.

3.3 Capability design

Java programmers use interfaces as contracts to work with each other — it specifies what tasks are in the contract, what the expected results should be, and a flexible contract is best not to be too strict. Java’s interfaces provide a sufficiently abstract architectural implementation, but there are still limitations.

Suppose we are now in the Java world view and have a need to “move a heavy object”, for which we have designed an abstract helpMoving method. If we were looking far enough ahead, we would put this method in an interface waiting to be implemented by other enthusiastic classes:

interface Helper{
    void helpMoving(a);
}

// Helper helper = new ...
// helper.helpMoving();
Copy the code

Any class capable of lifting heavy objects needs to implement this interface when it is defined, whether it is Man, Woman, Elephant, or even the more abstract Human and Animal.

Groovy, with its ability to run dynamically, argues that as long as it can do this, it doesn’t care who does it, and under which contract – as long as it has the ability to implement the helpMoving method, it doesn’t care who it is or what interface it implements.

// Groovy does not impose any restrictions on helper, so it is essentially Object.
def takeHelp(helper){
	helper.helpMoving()
}
Copy the code

The helper is essentially an Object class, and when we write this code, we don’t guarantee that it has a proper helpMoving method. (In Java terms, this code is dead wrong.) However, Groovy trusts that we’ll pass in a reasonable helper at runtime, so it doesn’t “block” it.

The helper does not explicitly implement the helper interface, so we are relying on one of its capabilities. Such a class is called a “duck type” in Python, and it’s alluded to as “if it walks like a duck and quacks like a duck, it’s a duck”.

Obviously, as helper requirements become more complex, incoming objects no longer need to supplement the source hierarchy by declaring that they implement this or that interface. The result is less red tape and lower development costs.

3.4 Treat dynamic types properly

There is no free lunch. Dynamic typing saves a certain amount of code, but one of the costs that comes with it is an increase in testing costs. 1. The compiler will no longer voluntarily do some checks for us, which means we need more unit tests to make it safe to run, or we’re playing with fire.

Suppose it is indeed possible for the user to pass in an “underpowered” object? There is no reason to worry. Groovy provides a way to “qualify” the helper at runtime, such as:

// Groovy does not impose any restrictions on helper, which is essentially Object.
def takeHelp(helper){
    // The contents of metaClass will be discussed later.
    if (helper.metaClass.respondsTo(helper,'helpMoving')) {
        helper.helpMoving()
    }else throw new Exception('this helper has no capacity of \'helpMoving\'')}class Elephant{
    def helpMoving(){
        println "doing that"
    }
}

takeHelp(new Elephant())
Copy the code

3.5 Method polymorphism in Groovy

First, you need to understand one big premise: Polymorphic methods implemented through inheritance relationships are dynamically selected (or “dynamically bound”) by Java at run time based on the actual type of the caller. Here are some examples of classes:

class Factory{
    public void make(Product product){
        System.out.println("make product by factory."); }}class ClothFactory extends Factory{
    @Override
    public void make(Product product) {
        System.out.println("make product by clothFactory.");
    }

    public void make(Cloth cloth){
        System.out.println("make cloth by clothFactory"); }}class Product{}class Cloth extends Product{}Copy the code

Dynamic binding is often used to explain method calls to upper transition objects, such as the clothFactory object in the code block below:

// Common objects
Factory factory = new Factory();

// On the transition object
Factory clothFactory = new ClothFactory();

// make product by factory.
factory.make(new Product());

// make product by clothFactory.
clothFactory.make(new Product());
Copy the code

In this case, however, the ClothFactory class, in addition to inheriting and overwriting the make method from its parent, also implements an overloaded make method that receives the Cloth type itself. But for Java, as long as the clothFactory reference is Factory, the overloaded make method can never be routed to, even if an instance of Cloth is passed in.

// Common objects
Factory factory = new Factory();

// On the transition object
Factory clothFactory = new ClothFactory();

// make product by factory.
factory.make(new Cloth());

// make product by clothFactory.
// Expectations did not meet.
clothFactory.make(new Cloth());
Copy the code

We treat clothFactory as a Factory type, so when we call its make method, the argument is always treated as the Product class, which is how the parent class is defined.

Groovy may understand developers better. Its method polymorphism is related not only to the actual type of the reference, but also to the actual type of the parameter (not only method polymorphism, but also the parameter becomes “polymorphic”, this method dispatching-based on multiple entities, hence the noun “multi-dispatch” or multi-method Multimethods). As a result, Groovy always finds the most appropriate way to solve a requirement. The code logic below is exactly the same as above, but in Groovy this results in a “more precise” execution.

class Factory{
    def make(Product product){print "make product by factory"}}class ClothFactory extends Factory{
    @Override
    def make(Product product) {print "make product by clothFactory."}
    def make(Cloth cloth){print "make cloth by clothFactory"}}class Product{}
class Cloth extends Product{}

Factory factory = new Factory()
Factory clothFactory = new ClothFactory()

// make product by factory
factory.make(new Cloth())

// make cloth by clothFactory
clothFactory.make(new Cloth())
Copy the code

3.6 Returning to the Static Type

Groovy is free to switch between dynamic and static. The choice depends largely on each programmer’s programming preferences. I’m used to the Java style, so for the most part I’ll opt for rigorous type checking, especially when defining classes. Where it is obvious to infer attribute/return value types, I tend to use the def keyword. Such as:

// Java:
// Tuple3<String,String,String> tps = new Tuple3<>("java","groovy","scala");
def tps = new Tuple3<>("java"."groovy"."scala")
Copy the code

Here are two useful annotations to make Groovy code live and quiet.

3.6.1 Strict compile-time checking

TypeChecked is an annotation that applies to a method or class. Once this annotation is annotated, Groovy does rigorous type checking within this block of code, rather than on the assumption that the runtime might be reasonable. In the previous example, the @typechecked annotation reverses the “duck type” — the compiler will now report an error.

import groovy.transform.TypeChecked

@TypeChecked
def takeHelp(helper){
    helper.helpMoving()
}
Copy the code

The above illustrates how this annotation applies to methods. If this annotation applies to a class, the Groovy compiler rigorously checks all of its internal methods, closures, and inner classes.

3.6.2 Static Compilation

Groovy’s dynamic typing comes at a cost of “minimal” performance — especially before JDK 7, when JVMS introduced invokeDynamic bytecodes. At the time, for programmers who sought Groovy’s simplicity but struggled with performance, simply turning Groovy into static code might have been a better choice. To do so would mean giving up all the convenience of dynamic typing in this chapter in exchange for better performance.

import groovy.transform.CompileStatic
import static java.lang.System.currentTimeMillis

// display the annotation parameter, return the type of the basic data type int.
@CompileStatic
int Fibonacci(int n){
    return (n<2)?1 : Fibonacci(n- 1) + Fibonacci(n2 -)
}

t1 = currentTimeMillis()
result = Fibonacci(40)
/ / result = Fib_more (40,1,1)
t2 = currentTimeMillis()

println "result:${result}"
println "time:${t2-t1} millis"


// Recursive Fibonacci sequence.
int Fib_more(int n,int left,int right){
    return (n == 0)? left : Fib_more(n- 1,right,(left + right))
}
Copy the code

The code above tests the impact of static compilation on performance by testing a non-tail recursive Fibonacci sequence. If the function annotates @compliestatic, it takes about 300 ms to run. In the case of no static compilation, the running time of this function is about 1200 ms, and the performance of this function is significantly reduced.

Note: Groovy is positioned as a scripting language (glue language), and it’s not really good at doing direct computationally intensive tasks. As we’ll see later, the real power of Groovy is its metaprogramming capability, which allows us to design internal DSLS. Think about why AI libraries today are built with C/C++ cores and then covered with a thin layer in Python: it’s a decision made for both performance and usability. So, an analogy to thinking about how Java, Groovy, and Scala should work together on the JVM…… Or are you interested in C?

3.7 Reference Materials


  1. Is Python strongly typed or weakly typed?↩
  2. I see two costs of dynamic typing: security and performance. ↩