Defining type-checking rules for dynamic code

The defining characteristic of Groovy is probably the dynamic nature of the language. Dynamic languages possess the capacity to extend a program at runtime, including changing types, behaviors, and object structures. With these languages, the things that static languages do at compile time can be done at runtime; we can even execute program statements that are created on the fly at runtime. Another trait of Groovy, typical of other dynamic languages such as Ruby or Python, is dynamic typing. With dynamic typing, the language automatically tests properties when expressions are being (dynamically) evaluated (that is, at runtime).

In this recipe, we will present how Groovy can be instructed to apply static typing checks to code elements to warn you about potential violations at compiletime; that is, before code is executed.

Getting ready

Let's look at some script as follows:

def name = 'john'
printn naame

The Groovy compiler groovyc is perfectly happy to compile the previous code. Can you spot the error?

But, if we try to execute the code we get, not surprisingly, the following exception:

Caught: groovy.lang.MissingPropertyException:
  No such property: naame
  for class: DynamicTypingExample

Dynamic typing is a very controversial topic. Proponents of static typing argue that the benefits of static typing encompass:

  1. Easier to spot programming mistakes (for example, preventing assigning a Boolean to an integer).
  2. Self-documenting code thanks to type signatures.
  3. The compiler has more opportunities to optimize the code (for example, replacing a virtual call with a direct one when the receiver's type is statically known).
  4. More efficient development tools (for example, autocompletion of members of a class).

Defenders of dynamic typing return fire by claiming that static typing is inflexible and has a negative impact on prototyping systems with volatile or unknown requirements. Furthermore, writing code in dynamic languages requires more discipline than writing in statically typed languages. This is often translated into a greater awareness for testing—and unit testing in particular—among developers who decide to produce code with dynamic languages.

It is worth noting that Groovy also supports optional static typing, even though the name is slightly misleading. In Groovy, we can declare a variable's type as follows:

int var = 10

or

String computeFrequency(List<String> listOfWords) {  }

Still, the compiler would not catch any compilation error if we assigned the wrong type to a typed variable:

int var = "Hello, I'm not a number!"

Groovy has optional typing mostly for Java compatibility and to support better code readability.

To mitigate some of the criticism inherent to dynamic typing, the Groovy team has introduced static type-checking since v2.0 of the language. Static type-checking enables the verification of the proper type and ensures that the methods we call and properties we access are valid for the type at compile time, hence decreasing the number of bugs pushed to the runtime.

How to do it...

Forcing the compiler to enable static type-checking it is simply a matter of annotating the code that we want to be checked with the @groovy.transform.TypeChecked annotation. The annotation can be placed on classes or individual methods:

  1. Create a new Groovy file (with the .groovy extension) with the following code:
    def upperInteger(myInteger) {
      println myInteger.toUpperCase()
    }
    
    upperInteger(10)
  2. From the shell, compile the code using the Groovy compiler groovyc. The compiler shouldn't report any error.
  3. Now, run the script using groovy. The runtime should throw an exception:
    caught: groovy.lang.MissingMethodException:
    No signature of method: java.lang.Integer.toUpperCase()
      is applicable for argument types: () values: []
    
  4. Add the type-checking annotation to the upperInteger function:
    @groovy.transform.TypeChecked
    def upperInteger(myInteger) {
      println myInteger.toUpperCase()
    }
  5. Compile the code again with groovyc. This time, the compiler reports the type error:
    DynamicTypingExample.groovy: 3: [Static type checking] -
    Cannot find matching method java.lang.Object#toUpperCase().
    Please check if the declared type is right
    and if the method exists.
    @ line 3, column 11.
    println myInteger.toUpperCase()
            ^
    

How it works...

If we place the annotation on a class, then type-checking is performed on all the methods, closures, and inner classes in the class. If we place it on a method, the type-checking is performed only on the members of the target method, including closures.

Static type-checking is implemented through an AST Transformation (see more on AST Transformations in Chapter 9, Metaprogramming and DSLs in Groovy) and can be used to enforce proper typing for a number of different cases. The following list shows a collection of bugs that would not be caught by the compiler but are caught by the TypeChecked annotation.

  1. Variable names:
    @TypeChecked
    def variableName() {
      def name = 'hello'
      println naame
    }

    The variable named name is misspelled in the second line. The compiler outputs:

    MyClass.groovy: 7: [Static type checking] - The variable [naame] is undeclared..
  2. GStrings:
    @TypeChecked
    def gstring() {
      def name = 'hello'
      println "hello, this is $naame"
    }

    Similar to the previous bug, the compiler complains about the undeclared variable naame.

  3. Collections:
    @TypeChecked
    def collections() {
      List myList = []
      myList = "I'm a String"
    }

    Here, the compiler detects that we are assigning a String to a List and throws an error: [Static type checking] - Cannot assign value of type java.lang.String to variable of type java.util.List.

  4. Collection type:
    @TypeChecked
    def moreCollections() {
      int[] array = new int[2]
      array[0] = 100
      array[1] = '100'
    }

    Similarly to the previous example, the compiler detects the invalid assignment of a String to an element of an array of ints.

  5. Return type:
    @TypeChecked
    int getSalary() {
      'fired on ' + new Date().format('yyyy-MM-dd')
    }

    Return types are also verified by the static type-checking algorithm. In this example, the compiler throws the following error:

    [Static type checking] - Cannot return value of type java.lang.String on method returning type int
    
  6. Return type propagation:
    boolean test() {
      true
    }
    @TypeChecked
    void m() {
      int x = 1 + test()
    }

    Wrong return types are also detected when the type is propagated from another method. In the previous example, the compiler fails with:

    Static type checking] - Cannot find matching method int#plus(void). Please check if the declared type is right and if the method exists
    
  7. Generics:
    @TypeChecked
    def generics() {
      List<Integer> aList = ['a','b']
    }

    Generics are supported as well. We are not allowed to assign the wrong type to a typed List.

There's more...

As you may have suspected, static type-checking doesn't play very nice with a dynamic language. Metaprogramming capabilities are effectively shut down when using the @TypeChecked annotation on a class or method.

Here is an example that uses metaprogramming to inject a new method into the class String (the Dynamically extending classes with new methods recipe in Chapter 9, Metaprogramming and DSLs in Groovy has more information about method injections):

class Sample {
  def test() {
    String.metaClass.hello = { "Hello, $delegate"  }
    println 'some String'.hello()
  }
}
new Sample().test()

When executed, this script yields the following output:

Hello, some String

If we add the @TypeChecked annotation to the class and try to run the script, we get the following error from the compiler:

[Static type checking] -
Cannot find matching method java.lang.String#hello().
Please check if the declared type is rightand if the method exists.

The reason for the error is that the compiler doesn't see the new dynamic method and therefore fails. Another case where static type-checking is too picky is when using implicit parameters in closures. Let's take an example:

class Sample {
  def test() {
    ['one','two','three'].collect { println it.toUpperCase() }
  }
}
new Sample().test()

This example, when executed, prints the following output:

ONE
TWO
THREE

If the Sample class is annotated with @TypeChecked, the compiler will return:

[Static type checking] -
Cannot find matching method java.lang.Object#toUpperCase().
Please check if the declared type is right
and if the method exists.
@ line 77, column 44.
wo','three'].collect { println it.toUpper
                              ^

What is happening here? Simply, the compiler has no idea of the type of the implicit variable it, which is often used inside closures to make the code even more terse. This compilation error can be solved by explicitly declaring the closure variable as follows:

['one','two','three'].collect {
  myString -> println myString.toUpperCase()
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset