This chapter covers the semantics of the Groovy programming language.
Variables can be defined using either their type (likeString) or by using the keyworddef followed by a variable name:
String xdef ydef acts as a type placeholder, i.e. a replacement for the type name,when you do not want to give an explicit type.It could be that you don’t care about the type at compile timeor are relying on type inference (with Groovy’s static nature).It is mandatory for variable definitions to have a type or placeholder.If left out, the type name will be deemed to refer to an existing variable (presumably declared earlier).For scripts, undeclared variables are assumed to come from the Script binding.In other cases, you will get a missing property (dynamic Groovy) or compile time error (static Groovy).If you think ofdef as an alias ofObject, you will understand in an instant.
Variable definitions can provide an initial value,in which case it’s like having a declaration and assignment (which we cover next) all in one.
Variable definition types can be refined by using generics, like inList<String> names.To learn more about the generics support, please read thegenerics section. |
Java introduced the In the context of variable definitions, you can think of |
You can assign values to variables for later use. Try the following:
x = 1println xx = new java.util.Date()println xx = -3.1499392println xx = falseprintln xx = "Hi"println xGroovy supports multiple assignment, i.e. where multiple variables can be assigned at once, e.g.:
def (a, b, c) = [10, 20, 'foo']assert a == 10 && b == 20 && c == 'foo'You can provide types as part of the declaration if you wish:
def (int i, String j) = [10, 'foo']assert i == 10 && j == 'foo'As well as used when declaring variables it also applies to existing variables:
def nums = [1, 3, 5]def a, b, c(a, b, c) = numsassert a == 1 && b == 3 && c == 5The syntax works for arrays as well as lists, as well as methods that return either of these:
def (_, month, year) = "18th June 2009".split()assert "In $month of $year" == 'In June of 2009'If the left hand side has too many variables, excess ones are filled with null’s:
def (a, b, c) = [1, 2]assert a == 1 && b == 2 && c == nullIf the right hand side has too many variables, the extra ones are ignored:
def (a, b) = [1, 2, 3]assert a == 1 && b == 2In the section describing Groovy’s operators,the case of thesubscript operator has been covered,explaining how you can override thegetAt()/putAt() method.
With this technique, we can combine multiple assignments and the subscript operator methods to implementobject destructuring.
Consider the following immutableCoordinates class, containing a pair of longitude and latitude doubles,and notice our implementation of thegetAt() method:
@Immutableclass Coordinates { double latitude double longitude double getAt(int idx) { if (idx == 0) latitude else if (idx == 1) longitude else throw new Exception('Wrong coordinate index, use 0 or 1') }}Now let’s instantiate this class and destructure its longitude and latitude:
def coordinates = new Coordinates(latitude: 43.23, longitude: 3.67)(1)def (la, lo) = coordinates(2)assert la == 43.23(3)assert lo == 3.67| 1 | we create an instance of theCoordinates class |
| 2 | then, we use a multiple assignment to get the individual longitude and latitude values |
| 3 | and we can finally assert their values. |
Groovy supports the usual if - else syntax from Java
def x = falsedef y = falseif ( !x ) { x = true}assert x == trueif ( x ) { x = false} else { y = true}assert x == yGroovy also supports the normal Java "nested" if then else if syntax:
if ( ... ) { ...} else if (...) { ...} else { ...}The switch statement in Groovy is backwards compatible with Java code; so you can fall through cases sharing the same code for multiple matches.
One difference though is that the Groovy switch statement can handle any kind of switch value and different kinds of matching can be performed.
def x = 1.23def result = ""switch (x) { case "foo": result = "found foo" // lets fall through case "bar": result += "bar" case [4, 5, 6, 'inList']: result = "list" break case 12..30: result = "range" break case Integer: result = "integer" break case Number: result = "number" break case ~/fo*/: // toString() representation of x matches the pattern? result = "foo regex" break case { it < 0 }: // or { x < 0 } result = "negative" break default: result = "default"}assert result == "number"Switch supports the following kinds of comparisons:
Class case values match if the switch value is an instance of the class
Regular expression case values match if thetoString() representation of the switch value matches the regex
Collection case values match if the switch value is contained in the collection. This also includes ranges (since they are Lists)
Closure case values match if the calling the closure returns a result which is true according to theGroovy truth
If none of the above are used then the case value matches if the case value equals the switch value
When using a closure case value, the defaultit parameter is actually the switch value (in our example, variablex). |
Groovy also supports switch expressions as shown in the following example:
def partner = switch(person) { case 'Romeo' -> 'Juliet' case 'Adam' -> 'Eve' case 'Antony' -> 'Cleopatra' case 'Bonnie' -> 'Clyde'}Groovy supports the standard Java / C for loop:
String message = ''for (int i = 0; i < 5; i += 1) { message += 'Hi '}assert message == 'Hi Hi Hi Hi Hi 'The more elaborate form of Java’s classic for loop with comma-separate expressionsis now supported. Example:
def facts = []def count = 5for (int fact = 1, i = 1; i <= count; i++, fact *= i) { facts << fact}assert facts == [1, 2, 6, 24, 120]Groovy has supported multi-assignment statements since Groovy 1.6:
// multi-assignment with typesdef (String x, int y) = ['foo', 42]assert "$x $y" == 'foo 42'These can now appear in for loops:
// multi-assignment goes loopydef baNums = []for (def (String u, int v) = ['bar', 42]; v < 45; u++, v++) { baNums << "$u $v"}assert baNums == ['bar 42', 'bas 43', 'bat 44']The for loop in Groovy is much simpler and works with any kind of array, collection, Map, etc.
// iterate over a rangedef x = 0for ( i in 0..9 ) { x += i}assert x == 45// iterate over a listx = 0for ( i in [0, 1, 2, 3, 4] ) { x += i}assert x == 10// iterate over an arrayx = 0for ( i in new int[]{0, 1, 2, 3, 4} ) { x += i}assert x == 10// iterate over a mapdef map = [a:1, b:2, c:3]x = 0for ( e in map ) { x += e.value}assert x == 6// iterate over values in a mapx = 0for ( v in map.values() ) { x += v}assert x == 6// iterate over the characters in a stringdef list = []for ( c in 'abc' ) { list.add(c)}assert list == ['a', 'b', 'c']// iterate with indexfor ( int i, k in map.keySet() ) { assert map.get(k) == i + 1}Groovy also supports the Java colon variation with colons:for (char c : text) {} |
Groovy supports the usual while {…} loops like Java:
def x = 0def y = 5while ( y-- > 0 ) { x++}assert x == 5Java’s class do/while loop is now supported. Example:
// classic Java-style do..while loopdef count = 5def fact = 1do { fact *= count--} while(count > 1)assert fact == 120Exception handling is the same as Java.
You can specify a completetry-catch-finally, atry-catch, or atry-finally set of blocks.
| Braces are required around each block’s body. |
try { 'moo'.toLong() // this will generate an exception assert false // asserting that this point should never be reached} catch ( e ) { assert e in NumberFormatException}We can put code within a 'finally' clause following a matching 'try' clause, so that regardless of whether the code in the 'try' clause throws an exception, the code in the finally clause will always execute:
def ztry { def i = 7, j = 0 try { def k = i / j assert false //never reached due to Exception in previous line } finally { z = 'reached here' //always executed even if Exception thrown }} catch ( e ) { assert e in ArithmeticException assert z == 'reached here'}With the multi catch block (since Groovy 2.0), we’re able to define several exceptions to be catch and treated by the same catch block:
try { /* ... */} catch ( IOException | NullPointerException e ) { /* one block to handle 2 exceptions */}Groovy often provides better alternatives to Java 7’stry-with-resources statement for Automatic Resource Management (ARM).That syntax is now supported for Java programmers migrating to Groovy and still wanting to use the old style:
class FromResource extends ByteArrayInputStream { @Override void close() throws IOException { super.close() println "FromResource closing" } FromResource(String input) { super(input.toLowerCase().bytes) }}class ToResource extends ByteArrayOutputStream { @Override void close() throws IOException { super.close() println "ToResource closing" }}def wrestle(s) { try ( FromResource from = new FromResource(s) ToResource to = new ToResource() ) { to << from return to.toString() }}def wrestle2(s) { FromResource from = new FromResource(s) try (from; ToResource to = new ToResource()) { // Enhanced try-with-resources in Java 9+ to << from return to.toString() }}assert wrestle("ARM was here!").contains('arm')assert wrestle2("ARM was here!").contains('arm')Which yields the following output:
ToResource closingFromResource closingToResource closingFromResource closing
Unlike Java with which Groovy shares theassert keyword, the latter in Groovy behaves very differently. First of all,an assertion in Groovy is always executed, independently of the-ea flag of the JVM. It makes this a first class choicefor unit tests. The notion of "power asserts" is directly related to how the Groovyassert behaves.
A power assertion is decomposed into 3 parts:
assert [left expression] == [right expression] : (optional message)
The result of the assertion is very different from what you would get in Java. If the assertion is true, then nothinghappens. If the assertion is false, then it provides a visual representation of the value of each sub-expressions of theexpression being asserted. For example:
assert 1+1 == 3Will yield:
Caught: Assertion failed:assert 1+1 == 3 | | 2 false
Power asserts become very interesting when the expressions are more complex, like in the next example:
def x = 2def y = 7def z = 5def calc = { a,b -> a*b+1 }assert calc(x,y) == [x,z].sum()Which will print the value for each sub-expression:
assert calc(x,y) == [x,z].sum() | | | | | | | 15 2 7 | 2 5 7 falseIn case you don’t want a pretty printed error message like above, you can fall back to a custom error message bychanging the optional message part of the assertion, like in this example:
def x = 2def y = 7def z = 5def calc = { a,b -> a*b+1 }assert calc(x,y) == z*z : 'Incorrect computation result'Which will print the following error message:
Incorrect computation result. Expression: (calc.call(x, y) == (z * z)). Values: z = 5, z = 5Any statement can be associated with a label. Labels do not impact the semantics of the code and can be used to makethe code easier to read like in the following example:
given: def x = 1 def y = 2when: def z = x+ythen: assert z == 3Despite not changing the semantics of the labelled statement, it is possible to use labels in thebreak instructionas a target for jump, as in the next example. However, even if this is allowed, this coding style is in general considereda bad practice:
for (int i=0;i<10;i++) { for (int j=0;j<i;j++) { println "j=$j" if (j == 5) { break exit } } exit: println "i=$i"}It is important to understand that by default labels have no impact on the semantics of the code, however they belong to the abstractsyntax tree (AST) so it is possible for an AST transformation to use that information to perform transformations overthe code, hence leading to different semantics. This is in particular what theSpock Frameworkdoes to make testing easier.
Expressions are the building blocks of Groovy programs that are used to referenceexisting values and execute code to create new ones.
Groovy supports many of the same kinds of expressions as Java, including:
Example expression(s) | Description |
| the name of a variable, field, parameter, … |
| special names |
| literals |
| Class literal |
| parenthesised expressions |
| Unaryoperator expressions |
| Binaryoperator expressions |
| Ternaryoperator expressions |
| Lambda expressions |
| switch expressions |
Groovy also has some of its own special expressions:
Example expression(s) | Description |
| Abbreviated class literal (when not ambiguous) |
| Closure expressions |
| literal list expressions |
| literal map expressions |
Groovy also expands on the normal dot-notation used in Java for member access.Groovy provides special support for accessing hierarchical data structures by specifying thepath in the hierarchy of some data of interest.TheseGroovy path expressions are known as GPath expressions.
GPath is a path expression language integrated into Groovy which allows parts of nested structured data to be identified. In thissense, it has similar aims and scope as XPath does for XML. GPath is often used in the context of processing XML, but it really appliesto any object graph. Where XPath uses a filesystem-like path notation, a tree hierarchy with parts separated by a slash/, GPathuse adot-object notation to perform object navigation.
As an example, you can specify a path to an object or element of interest:
a.b.c → for XML, yields all thec elements insideb insidea
a.b.c → for POJOs, yields thec properties for all theb properties ofa (sort of likea.getB().getC() in JavaBeans)
In both cases, the GPath expression can be viewed as a query on an object graph. For POJOs, the object graph is most often built by theprogram being written through object instantiation and composition; for XML processing, the object graph is the result ofparsingthe XML text, most often with classes like XmlParser or XmlSlurper. SeeProcessing XMLfor more in-depth details on consuming XML in Groovy.
When querying the object graph generated from XmlParser or XmlSlurper, a GPath expression can refer to attributes defined on elements withthe
|
Let’s see an example of a GPath expression on a simpleobject graph, the one obtained using java reflection. Suppose you are in a non-static method of aclass having another method namedaMethodFoo
void aMethodFoo() { println "This is aMethodFoo." }(0)the following GPath expression will get the name of that method:
assert ['aMethodFoo'] == this.class.methods.name.grep(~/.*Foo/)More precisely, the above GPath expression produces a list of String, each being the name of an existing method onthis where that name ends withFoo.
Now, given the following methods also defined in that class:
void aMethodBar() { println "This is aMethodBar." }(1)void anotherFooMethod() { println "This is anotherFooMethod." }(2)void aSecondMethodBar() { println "This is aSecondMethodBar." }(3)then the following GPath expression will get the names of(1) and(3), but not(2) or(0):
assert ['aMethodBar', 'aSecondMethodBar'] as Set == this.class.methods.name.grep(~/.*Bar/) as SetWe can decompose the expressionthis.class.methods.name.grep(~/.*Bar/) to get an idea of how a GPath is evaluated:
this.classproperty accessor, equivalent tothis.getClass() in Java, yields aClass object.
this.class.methodsproperty accessor, equivalent tothis.getClass().getMethods(), yields an array ofMethod objects.
this.class.methods.nameapply a property accessor on each element of an array and produce a list of the results.
this.class.methods.name.grep(…)call methodgrep on each element of the list yielded bythis.class.methods.name and produce a list of the results.
A sub-expression likethis.class.methods yields an array because this is what callingthis.getClass().getMethods() in Javawould produce.GPath expressions do not have a convention where as means a list or anything like that. |
One powerful feature of GPath expression is that property access on a collection is converted to aproperty access on each element of the collection withthe results collected into a collection. Therefore, the expressionthis.class.methods.name could be expressed as follows in Java:
List<String> methodNames = new ArrayList<String>();for (Method method : this.getClass().getMethods()) { methodNames.add(method.getName());}return methodNames;Array access notation can also be used in a GPath expression where a collection is present :
assert 'aSecondMethodBar' == this.class.methods.name.grep(~/.*Bar/).sort()[1]| array access are zero-based in GPath expressions |
Here is an example with an XML document and various form of GPath expressions:
def xmlText = """ | <root> | <level> | <sublevel id='1'> | <keyVal> | <key>mykey</key> | <value>value 123</value> | </keyVal> | </sublevel> | <sublevel id='2'> | <keyVal> | <key>anotherKey</key> | <value>42</value> | </keyVal> | <keyVal> | <key>mykey</key> | <value>fizzbuzz</value> | </keyVal> | </sublevel> | </level> | </root> """def root = new XmlSlurper().parseText(xmlText.stripMargin())assert root.level.size() == 1(1)assert root.level.sublevel.size() == 2(2)assert root.level.sublevel.findAll { it.@id == 1 }.size() == 1(3)assert root.level.sublevel[1].keyVal[0].key.text() == 'anotherKey'(4)| 1 | There is onelevel node underroot |
| 2 | There are twosublevel nodes underroot/level |
| 3 | There is one elementsublevel having an attributeid with value1 |
| 4 | Text value ofkey element of firstkeyVal element of secondsublevel element underroot/level is 'anotherKey' |
Further details about GPath expressions for XML are in theXML User Guide.
The rules of number promotion are specified in the section onmath operations.
A SAM type is a type which defines a single abstract method. This includes:
interface Predicate<T> { boolean accept(T obj)}abstract class Greeter { abstract String getName() void greet() { println "Hello, $name" }}Any closure can be converted into a SAM type using theas operator:
Predicate filter = { it.contains 'G' } as Predicateassert filter.accept('Groovy') == trueGreeter greeter = { 'Groovy' } as Greetergreeter.greet()However, theas Type expression is optional since Groovy 2.2.0. You can omit it and simply write:
Predicate filter = { it.contains 'G' }assert filter.accept('Groovy') == trueGreeter greeter = { 'Groovy' }greeter.greet()which means you are also allowed to use method pointers, as shown in the following example:
boolean doFilter(String s) { s.contains('G') }Predicate filter = this.&doFilterassert filter.accept('Groovy') == trueGreeter greeter = GroovySystem.&getVersiongreeter.greet()The second and probably more important use case for closure to SAM type coercion is calling a method which acceptsa SAM type. Imagine the following method:
public <T> List<T> filter(List<T> source, Predicate<T> predicate) { source.findAll { predicate.accept(it) }}Then you can call it with a closure, without having to create an explicit implementation of the interface:
assert filter(['Java','Groovy'], { it.contains 'G'} as Predicate) == ['Groovy']But since Groovy 2.2.0, you are also able to omit the explicit coercion and call the method as if it used a closure:
assert filter(['Java','Groovy']) { it.contains 'G'} == ['Groovy']As you can see, this has the advantage of letting you use the closure syntax for method calls, that is to say put theclosure outside the parenthesis, improving the readability of your code.
In addition to SAM types, a closure can be coerced to any type and in particular interfaces. Let’s define thefollowing interface:
interface FooBar { int foo() void bar()}You can coerce a closure into the interface using theas keyword:
def impl = { println 'ok'; 123 } as FooBarThis produces a class for which all methods are implemented using the closure:
assert impl.foo() == 123impl.bar()But it is also possible to coerce a closure to any class. For example, we can replace theinterface that we definedwithclass without changing the assertions:
class FooBar { int foo() { 1 } void bar() { println 'bar' }}def impl = { println 'ok'; 123 } as FooBarassert impl.foo() == 123impl.bar()Usually using a single closure to implement an interface or a class with multiple methods is not the way to go. As analternative, Groovy allows you to coerce a map into an interface or a class. In that case, keys of the map areinterpreted as method names, while the values are the method implementation. The following example illustrates thecoercion of a map into anIterator:
def mapmap = [ i: 10, hasNext: { map.i > 0 }, next: { map.i-- },]def iter = map as IteratorOf course this is a rather contrived example, but illustrates the concept. You only need to implement those methodsthat are actually called, but if a method is called that doesn’t exist in the map aMissingMethodException or anUnsupportedOperationException is thrown, depending on the arguments passed to the call,as in the following example:
interface X { void f() void g(int n) void h(String s, int n)}x = [ f: {println "f called"} ] as Xx.f() // method existsx.g() // MissingMethodException herex.g(5) // UnsupportedOperationException hereThe type of the exception depends on the call itself:
MissingMethodException if the arguments of the call do not match those from the interface/class
UnsupportedOperationException if the arguments of the call match one of the overloaded methods of the interface/class
Groovy allows transparentString (orGString) to enum values coercion. Imagine you define the following enum:
enum State { up, down}then you can assign a string to the enum without having to use an explicitas coercion:
State st = 'up'assert st == State.upIt is also possible to use aGString as the value:
def val = "up"State st = "${val}"assert st == State.upHowever, this would throw a runtime error (IllegalArgumentException):
State st = 'not an enum value'Note that it is also possible to use implicit coercion in switch statements:
State switchState(State st) { switch (st) { case 'up': return State.down // explicit constant case 'down': return 'up' // implicit coercion for return types }}in particular, see how thecase use string constants. But if you call a method that uses an enum with aStringargument, you still have to use an explicitas coercion:
assert switchState('up' as State) == State.downassert switchState(State.down) == State.upIt is possible for a class to define custom coercion strategies by implementing theasType method. Custom coercionis invoked using theas operator and is never implicit. As an example,imagine you defined two classes,Polar andCartesian, like in the following example:
class Polar { double r double phi}class Cartesian { double x double y}And that you want to convert from polar coordinates to cartesian coordinates. One way of doing this is to definetheasType method in thePolar class:
def asType(Class target) { if (Cartesian==target) { return new Cartesian(x: r*cos(phi), y: r*sin(phi)) }}which allows you to use theas coercion operator:
def sigma = 1E-16def polar = new Polar(r:1.0,phi:PI/2)def cartesian = polar as Cartesianassert abs(cartesian.x-sigma) < sigmaPutting it all together, thePolar class looks like this:
class Polar { double r double phi def asType(Class target) { if (Cartesian==target) { return new Cartesian(x: r*cos(phi), y: r*sin(phi)) } }}but it is also possible to defineasType outside of thePolar class, which can be practical if you want to definecustom coercion strategies for "closed" classes or classes for which you don’t own the source code, for example usinga metaclass:
Polar.metaClass.asType = { Class target -> if (Cartesian==target) { return new Cartesian(x: r*cos(phi), y: r*sin(phi)) }}Using theas keyword is only possible if you have a static reference to a class, like in the following code:
interface Greeter { void greet()}def greeter = { println 'Hello, Groovy!' } as Greeter // Greeter is known staticallygreeter.greet()But what if you get the class by reflection, for example by callingClass.forName?
Class clazz = Class.forName('Greeter')Trying to use the reference to the class with theas keyword would fail:
greeter = { println 'Hello, Groovy!' } as clazz// throws:// unable to resolve class clazz// @ line 9, column 40.// greeter = { println 'Hello, Groovy!' } as clazzIt is failing because theas keyword only works with class literals. Instead, you need to call theasType method:
greeter = { println 'Hello, Groovy!' }.asType(clazz)greeter.greet()Method calls can omit the parentheses if there is at least one parameter and there is no ambiguity:
println 'Hello World'def maximum = Math.max 5, 10Parentheses are required for method calls without parameters or ambiguous method calls:
println()println(Math.max(5, 10))In Groovy semicolons at the end of the line can be omitted, if the line contains only a single statement.
This means that:
assert true;can be more idiomatically written as:
assert trueMultiple statements in a line require semicolons to separate them:
boolean a = true; assert aIn Groovy, the last expression evaluated in the body of a method or a closure is returned. This means that thereturn keyword is optional.
int add(int a, int b) { return a+b}assert add(1, 2) == 3Can be shortened to:
int add(int a, int b) { a+b}assert add(1, 2) == 3By default, Groovy classes and methods arepublic. Therefore this class:
public class Server { public String toString() { "a server" }}is identical to this class:
class Server { String toString() { "a server" }}Groovy decides whether an expression is true or false by applying the rules given below.
Iterators and Enumerations with further elements are coerced to true.
assert [0].iterator()assert ![].iterator()Vector v = [0] as VectorEnumeration enumeration = v.elements()assert enumerationenumeration.nextElement()assert !enumerationNon-empty Strings, GStrings and CharSequences are coerced to true.
assert 'a'assert !''def nonEmpty = 'a'assert "$nonEmpty"def empty = ''assert !"$empty"Non-null object references are coerced to true.
assert new Object()assert !nullIn order to customize whether groovy evaluates your object totrue orfalse implement theasBoolean() method:
class Color { String name boolean asBoolean(){ name == 'green' ? true : false }}Groovy will call this method to coerce your object to a boolean value, e.g.:
assert new Color(name: 'green')assert !new Color(name: 'red')Optional typing is the idea that a program can work even if you don’t put an explicit type on a variable. Being a dynamiclanguage, Groovy naturally implements that feature, for example when you declare a variable:
String aString = 'foo'(1)assert aString.toUpperCase()(2)| 1 | foo is declared using an explicit type,String |
| 2 | we can call thetoUpperCase method on aString |
Groovy will let you write this instead:
def aString = 'foo'(1)assert aString.toUpperCase()(2)| 1 | foo is declared usingdef |
| 2 | we can still call thetoUpperCase method, because the type ofaString is resolved at runtime |
So it doesn’t matter that you use an explicit type here. It is in particular interesting when you combine this featurewithstatic type checking, because the type checker performs type inference.
Likewise, Groovy doesn’t make it mandatory to declare the types of a parameter in a method:
String concat(String a, String b) { a+b}assert concat('foo','bar') == 'foobar'can be rewritten usingdef as both return type and parameter types, in order to take advantage of duck typing, asillustrated in this example:
def concat(def a, def b) {(1) a+b}assert concat('foo','bar') == 'foobar'(2)assert concat(1,2) == 3(3)| 1 | both the return type and the parameter types usedef |
| 2 | it makes it possible to use the method withString |
| 3 | but also withint since theplus method is defined |
Using thedef keyword here is recommended to describe the intent of a method which is supposed to work on anytype, but technically, we could useObject instead and the result would be the same:def is, in Groovy, strictlyequivalent to usingObject. |
Eventually, the type can be removed altogether from both the return type and the descriptor. But if you want to removeit from the return type, you then need to add an explicit modifier for the method, so that the compiler can make a differencebetween a method declaration and a method call, like illustrated in this example:
private concat(a,b) {(1) a+b}assert concat('foo','bar') == 'foobar'(2)assert concat(1,2) == 3(3)| 1 | if we want to omit the return type, an explicit modifier has to be set. |
| 2 | it is still possible to use the method withString |
| 3 | and also withint |
Omitting types is in general considered a bad practice in method parameters or method return types for public APIs.While usingdef in a local variable is not really a problem because the visibility of the variable is limited to themethod itself, while set on a method parameter,def will be converted toObject in the method signature, making itdifficult for users to know which is the expected type of the arguments. This means that you should limit this to caseswhere you are explicitly relying on duck typing. |
By default, Groovy performs minimal type checking at compile time. Since it is primarily a dynamic language,most checks that a static compiler would normally do aren’t possible at compile time. A method added via runtimemetaprogramming might alter a class or object’s runtime behavior. Let’s illustrate why in thefollowing example:
class Person {(1) String firstName String lastName}def p = new Person(firstName: 'Raymond', lastName: 'Devos')(2)assert p.formattedName == 'Raymond Devos'(3)| 1 | thePerson class only defines two properties,firstName andlastName |
| 2 | we can create an instance of Person |
| 3 | and call a method namedformattedName |
It is quite common in dynamic languages for code such as the above example not to throw any error. How can this be?In Java, this would typically fail at compile time. However, in Groovy, it will not fail at compile time, and if codedcorrectly, will also not fail at runtime. In fact, to make this work at runtime,one possibility is to rely onruntime metaprogramming. So just adding this line after the declaration of thePerson class is enough:
Person.metaClass.getFormattedName = { "$delegate.firstName $delegate.lastName" }This means that in general, in Groovy, you can’t make any assumption about the type of an object beyond its declarationtype, and even if you know it, you can’t determine at compile time what method will be called, or which property willbe retrieved. It has a lot of interest, going from writing DSLs to testing, which is discussed in other sections of thismanual.
However, if your program doesn’t rely on dynamic features and that you come from the static world (in particular, froma Java mindset), not catching such "errors" at compile time can be surprising. As we have seen in the previous example,the compiler cannot be sure this is an error. To make it aware that it is, you have to explicitly instruct the compilerthat you are switching to a type checked mode. This can be done by annotating a class or a method with@groovy.transform.TypeChecked.
When type checking is activated, the compiler performs much more work:
type inference is activated, meaning that even if you usedef on a local variable for example, the type checker will beable to infer the type of the variable from the assignments
method calls are resolved at compile time, meaning that if a method is not declared on a class, the compiler will throw an error
in general, all the compile time errors that you are used to find in a static language will appear: method not found, property not found,incompatible types for method calls, number precision errors, …
In this section, we will describe the behavior of the type checker in various situations and explain the limits of using@TypeChecked on your code.
@TypeChecked annotationThegroovy.transform.TypeChecked annotation enables type checking. It can be placed on a class:
@groovy.transform.TypeCheckedclass Calculator { int sum(int x, int y) { x+y }}Or on a method:
class Calculator { @groovy.transform.TypeChecked int sum(int x, int y) { x+y }}In the first case, all methods, properties, fields, inner classes, … of the annotated class will be type checked, whereasin the second case, only the method and potential closures or anonymous inner classes that it contains will be type checked.
The scope of type checking can be restricted. For example, if a class is type checked, you can instruct the type checkerto skip a method by annotating it with@TypeChecked(TypeCheckingMode.SKIP):
import groovy.transform.TypeCheckedimport groovy.transform.TypeCheckingMode@TypeChecked(1)class GreetingService { String greeting() {(2) doGreet() } @TypeChecked(TypeCheckingMode.SKIP)(3) private String doGreet() { def b = new SentenceBuilder() b.Hello.my.name.is.John(4) b }}def s = new GreetingService()assert s.greeting() == 'Hello my name is John'| 1 | theGreetingService class is marked as type checked |
| 2 | so thegreeting method is automatically type checked |
| 3 | butdoGreet is marked withSKIP |
| 4 | the type checker doesn’t complain about missing properties here |
In the previous example,SentenceBuilder relies on dynamic code. There’s no realHello method or property, so thetype checker would normally complain and compilation would fail. Since the method that uses the builder is marked withTypeCheckingMode.SKIP, type checking isskipped for this method, so the code will compile, even if the rest of theclass is type checked.
The following sections describe the semantics of type checking in Groovy.
An objecto of typeA can be assigned to a variable of typeT if and only if:
T equalsA
Date now = new Date()orT is one ofString,boolean,Boolean orClass
String s = new Date() // implicit call to toStringBoolean boxed = 'some string' // Groovy truthboolean prim = 'some string' // Groovy truthClass clazz = 'java.lang.String' // class coercionoro is null andT is not a primitive type
String s = null // passesint i = null // failsorT is an array andA is an array and the component type ofA is assignable to the component type ofT
int[] i = new int[4] // passesint[] i = new String[4] // failsorT is an array andA is a collection or stream and the component type ofA is assignable to the component type ofT
int[] i = [1,2,3] // passesint[] i = [1,2, new Date()] // failsSet set = [1,2,3]Number[] na = set // passesdef stream = Arrays.stream(1,2,3)int[] i = stream // passesorT is a superclass ofA
AbstractList list = new ArrayList() // passesLinkedList list = new ArrayList() // failsorT is an interface implemented byA
List list = new ArrayList() // passesRandomAccess list = new LinkedList() // failsorT orA are a primitive type and their boxed types are assignable
int i = 0Integer bi = 1int x = Integer.valueOf(123)double d = Float.valueOf(5f)orT extendsgroovy.lang.Closure andA is a SAM-type (single abstract method type)
Runnable r = { println 'Hello' }interface SAMType { int doSomething()}SAMType sam = { 123 }assert sam.doSomething() == 123abstract class AbstractSAM { int calc() { 2* value() } abstract int value()}AbstractSAM c = { 123 }assert c.calc() == 246orT andA derive fromjava.lang.Number and conform to the following table
| T | A | Examples |
|---|---|---|
Double | Any but BigDecimal or BigInteger | |
Float | Any type but BigDecimal, BigInteger or Double | |
Long | Any type but BigDecimal, BigInteger, Double or Float | |
Integer | Any type but BigDecimal, BigInteger, Double, Float or Long | |
Short | Any type but BigDecimal, BigInteger, Double, Float, Long or Integer | |
Byte | Byte | |
In addition to the assignment rules above, if an assignment is deemed invalid, in type checked mode, alist literal or amap literalA can be assignedto a variable of typeT if:
the assignment is a variable declaration andA is a list literal andT has a constructor whose parameters match the types of the elements in the list literal
the assignment is a variable declaration andA is a map literal andT has a no-arg constructor and a property for each of the map keys
For example, instead of writing:
@groovy.transform.TupleConstructorclass Person { String firstName String lastName}Person classic = new Person('Ada','Lovelace')You can use a "list constructor":
Person list = ['Ada','Lovelace']or a "map constructor":
Person map = [firstName:'Ada', lastName:'Lovelace']If you use a map constructor, additional checks are done on the keys of the map to check if a property of the same nameis defined. For example, the following will fail at compile time:
@groovy.transform.TupleConstructorclass Person { String firstName String lastName}Person map = [firstName:'Ada', lastName:'Lovelace', age: 24](1)| 1 | The type checker will throw an errorNo such property: age for class: Person at compile time |
In type checked mode, methods are resolved at compile time. Resolution works by name and arguments. The return type isirrelevant to method selection. Types of arguments are matched against the types of the parameters following those rules:
An argumento of typeA can be used for a parameter of typeT if and only if:
T equalsA
int sum(int x, int y) { x+y}assert sum(3,4) == 7orT is aString andA is aGString
String format(String str) { "Result: $str"}assert format("${3+4}") == "Result: 7"oro is null andT is not a primitive type
String format(int value) { "Result: $value"}assert format(7) == "Result: 7"format(null) // failsorT is an array andA is an array and the component type ofA is assignable to the component type ofT
String format(String[] values) { "Result: ${values.join(' ')}"}assert format(['a','b'] as String[]) == "Result: a b"format([1,2] as int[]) // failsorT is a superclass ofA
String format(AbstractList list) { list.join(',')}format(new ArrayList()) // passesString format(LinkedList list) { list.join(',')}format(new ArrayList()) // failsorT is an interface implemented byA
String format(List list) { list.join(',')}format(new ArrayList()) // passesString format(RandomAccess list) { 'foo'}format(new LinkedList()) // failsorT orA are a primitive type and their boxed types are assignable
int sum(int x, Integer y) { x+y}assert sum(3, new Integer(4)) == 7assert sum(new Integer(3), 4) == 7assert sum(new Integer(3), new Integer(4)) == 7assert sum(new Integer(3), 4) == 7orT extendsgroovy.lang.Closure andA is a SAM-type (single abstract method type)
interface SAMType { int doSomething()}int twice(SAMType sam) { 2*sam.doSomething() }assert twice { 123 } == 246abstract class AbstractSAM { int calc() { 2* value() } abstract int value()}int eightTimes(AbstractSAM sam) { 4*sam.calc() }assert eightTimes { 123 } == 984orT andA derive fromjava.lang.Number and conform to the same rules asassignment of numbers
If a method with the appropriate name and arguments is not found at compile time, an error is thrown. The difference with "normal" Groovy isillustrated in the following example:
class MyService { void doSomething() { printLine 'Do something'(1) }}| 1 | printLine is an error, but since we’re in a dynamic mode, the error is not caught at compile time |
The example above shows a class that Groovy will be able to compile. However, if you try to create an instance ofMyService and call thedoSomething method, then it will failat runtime, becauseprintLine doesn’t exist. Of course, we already showed how Groovy could makethis a perfectly valid call, for example by catchingMethodMissingException or implementing a custom metaclass, but if you know you’renot in such a case,@TypeChecked comes handy:
@groovy.transform.TypeCheckedclass MyService { void doSomething() { printLine 'Do something'(1) }}| 1 | printLine is this time a compile-time error |
Just adding@TypeChecked will trigger compile time method resolution. The type checker will try to find a methodprintLine acceptingaString on theMyService class, but cannot find one. It will fail compilation with the following message:
Cannot find matching method MyService#printLine(java.lang.String)
It is important to understand the logic behind the type checker: it is a compile-time check, so by definition, the type checkeris not aware of any kind ofruntime metaprogramming that you do. This means that code which is perfectly valid without@TypeChecked willnot compile anymore if you activate type checking. This is in particular true if you think of duck typing: |
class Duck { void quack() {(1) println 'Quack!' }}class QuackingBird { void quack() {(2) println 'Quack!' }}@groovy.transform.TypeCheckedvoid accept(quacker) { quacker.quack()(3)}accept(new Duck())(4)| 1 | we define aDuck class which defines aquack method |
| 2 | we define anotherQuackingBird class which also defines aquack method |
| 3 | quacker is loosely typed, so since the method is@TypeChecked, we will obtain a compile-time error |
| 4 | even if in non type-checked Groovy, this would have passed |
There are possible workarounds, like introducing an interface, but basically, by activating type checking, you gain type safetybut you loose some features of the language. Hopefully, Groovy introduces some features like flow typing to reduce the gap betweentype-checked and non type-checked Groovy.
When code is annotated with@TypeChecked, the compiler performs type inference. It doesn’t simply rely on static types, but also uses varioustechniques to infer the types of variables, return types, literals, … so that the code remains as clean as possible even if you activate thetype checker.
The simplest example is inferring the type of a variable:
def message = 'Welcome to Groovy!'(1)println message.toUpperCase()(2)println message.upper() // compile time error(3)| 1 | a variable is declared using thedef keyword |
| 2 | callingtoUpperCase is allowed by the type checker |
| 3 | callingupper will fail at compile time |
The reason the call totoUpperCase works is because the type ofmessage wasinferred as being aString.
It is worth noting that although the compiler performs type inference on local variables, it doesnot perform any kindof type inference on fields, always falling back to thedeclared type of a field. To illustrate this, let’s take alook at this example:
class SomeClass { def someUntypedField(1) String someTypedField(2) void someMethod() { someUntypedField = '123'(3) someUntypedField = someUntypedField.toUpperCase() // compile-time error(4) } void someSafeMethod() { someTypedField = '123'(5) someTypedField = someTypedField.toUpperCase()(6) } void someMethodUsingLocalVariable() { def localVariable = '123'(7) someUntypedField = localVariable.toUpperCase()(8) }}| 1 | someUntypedField usesdef as a declaration type |
| 2 | someTypedField usesString as a declaration type |
| 3 | we can assignanything tosomeUntypedField |
| 4 | yet callingtoUpperCase fails at compile time because the field is not typed properly |
| 5 | we can assign aString to a field of typeString |
| 6 | and this timetoUpperCase is allowed |
| 7 | if we assign aString to a local variable |
| 8 | then callingtoUpperCase is allowed on the local variable |
Why such a difference? The reason isthread safety. At compile time, we can’t makeany guarantee about the type ofa field. Any thread can access any field at any time and between the moment a field is assigned a variable of sometype in a method and the time is used the line after, another thread may have changed the contents of the field. Thisis not the case for local variables: we know if they "escape" or not, so we can make sure that the type of a variable isconstant (or not) over time. Note that even if a field is final, the JVM makes no guarantee about it, so the type checkerdoesn’t behave differently if a field is final or not.
This is one of the reasons why we recommend to usetyped fields. While usingdef for local variables is perfectlyfine thanks to type inference, this is not the case for fields, which also belong to the public API of a class, hence thetype is important. |
Groovy provides a syntax for various type literals. There are three native collection literals in Groovy:
lists, using the[] literal
maps, using the[:] literal
ranges, usingfrom..to (inclusive),from..<to (right exclusive),from<..to (left exclusive) andfrom<..<to (full exclusive)
The inferred type of a literal depends on the elements of the literal, as illustrated in the following table:
| Literal | Inferred type |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
As you can see, with the noticeable exception of theIntRange, the inferred type makes use of generics types to describethe contents of a collection. In case the collection contains elements of different types, the type checker still performstype inference of the components, but uses the notion ofleast upper bound.
In Groovy, theleast upper bound of two typesA andB is defined as a type which:
superclass corresponds to the common super class ofA andB
interfaces correspond to the interfaces implemented by bothA andB
ifA orB is a primitive type and thatA isn’t equal toB, the least upper bound ofA andB is the leastupper bound of their wrapper types
IfA andB only have one (1) interface in common and that their common superclass isObject, then the LUB of bothis the common interface.
The least upper bound represents the minimal type to which bothA andB can be assigned. So for example, ifA andBare bothString, then the LUB (least upper bound) of both is alsoString.
class Top {}class Bottom1 extends Top {}class Bottom2 extends Top {}assert leastUpperBound(String, String) == String(1)assert leastUpperBound(ArrayList, LinkedList) == AbstractList(2)assert leastUpperBound(ArrayList, List) == List(3)assert leastUpperBound(List, List) == List(4)assert leastUpperBound(Bottom1, Bottom2) == Top(5)assert leastUpperBound(List, Serializable) == Object(6)| 1 | the LUB ofString andString isString |
| 2 | the LUB ofArrayList andLinkedList is their common super type,AbstractList |
| 3 | the LUB ofArrayList andList is their only common interface,List |
| 4 | the LUB of two identical interfaces is the interface itself |
| 5 | the LUB ofBottom1 andBottom2 is their superclassTop |
| 6 | the LUB of two types which have nothing in common isObject |
In those examples, the LUB is always representable as a normal, JVM supported, type. But Groovy internally represents the LUBas a type which can be more complex, and that you wouldn’t be able to use to define a variable for example. To illustrate this,let’s continue with this example:
interface Foo {}class Top {}class Bottom extends Top implements Serializable, Foo {}class SerializableFooImpl implements Serializable, Foo {}What is the least upper bound ofBottom andSerializableFooImpl? They don’t have a common super class (apart fromObject),but they do share 2 interfaces (Serializable andFoo), so their least upper bound is a type which represents the union oftwo interfaces (Serializable andFoo). This type cannot be defined in the source code, yet Groovy knows about it.
In the context of collection type inference (and generic type inference in general), this becomes handy, because the type of thecomponents is inferred as the least upper bound. We can illustrate why this is important in the following example:
interface Greeter { void greet() }(1)interface Salute { void salute() }(2)class A implements Greeter, Salute {(3) void greet() { println "Hello, I'm A!" } void salute() { println "Bye from A!" }}class B implements Greeter, Salute {(4) void greet() { println "Hello, I'm B!" } void salute() { println "Bye from B!" } void exit() { println 'No way!' }(5)}def list = [new A(), new B()](6)list.each { it.greet()(7) it.salute()(8) it.exit()(9)}| 1 | theGreeter interface defines a single method,greet |
| 2 | theSalute interface defines a single method,salute |
| 3 | classA implements bothGreeter andSalute but there’s no explicit interface extending both |
| 4 | same forB |
| 5 | butB defines an additionalexit method |
| 6 | the type oflist is inferred as "list of the LUB ofA and `B`" |
| 7 | so it is possible to callgreet which is defined on bothA andB through theGreeter interface |
| 8 | and it is possible to callsalute which is defined on bothA andB through theSalute interface |
| 9 | yet callingexit is a compile time error because it doesn’t belong to the LUB ofA andB (only defined inB) |
The error message will look like:
[Static type checking] - Cannot find matching method Greeter or Salute#exit()
which indicates that theexit method is neither defines onGreeter norSalute, which are the two interfaces definedin the least upper bound ofA andB.
In normal, non type checked, Groovy, you can write things like:
class Greeter { String greeting() { 'Hello' }}void doSomething(def o) { if (o instanceof Greeter) {(1) println o.greeting()(2) }}doSomething(new Greeter())| 1 | guard the method call with aninstanceof check |
| 2 | make the call |
The method call works because of dynamic dispatch (the method is selected at runtime). The equivalent code in Java wouldrequire to casto to aGreeter before calling thegreeting method, because methods are selected at compile time:
if (o instanceof Greeter) { System.out.println(((Greeter)o).greeting());}However, in Groovy, even if you add@TypeChecked (and thus activate type checking) on thedoSomething method, thecast isnot necessary. The compiler embedsinstanceof inference that makes the cast optional.
Flow typing is an important concept of Groovy in type checked mode and an extension of type inference. The idea is thatthe compiler is capable of inferring the type of variables in the flow of the code, not just at initialization:
@groovy.transform.TypeCheckedvoid flowTyping() { def o = 'foo'(1) o = o.toUpperCase()(2) o = 9d(3) o = Math.sqrt(o)(4)}| 1 | first,o is declared usingdef and assigned aString |
| 2 | the compiler inferred thato is aString, so callingtoUpperCase is allowed |
| 3 | o is reassigned with adouble |
| 4 | callingMath.sqrt passes compilation because the compiler knows that at this point,o is adouble |
So the type checker isaware of the fact that the concrete type of a variable is different over time. In particular,if you replace the last assignment with:
o = 9do = o.toUpperCase()The type checker will now fail at compile time, because it knows thato is adouble whentoUpperCase is called,so it’s a type error.
It is important to understand that it is not the fact of declaring a variable withdef that triggers type inference.Flow typing works forany variable of any type. Declaring a variable with an explicit type only constrains what youcan assign to the variable:
@groovy.transform.TypeCheckedvoid flowTypingWithExplicitType() { List list = ['a','b','c'](1) list = list*.toUpperCase()(2) list = 'foo'(3)}| 1 | list is declared as an uncheckedList and assigned a list literal of strings |
| 2 | this line passes compilation because of flow typing: the type checker knows thatlist is at this point anArrayList<String> |
| 3 | but you can’t assign aString to aList so this is a type checking error |
You can also note that even if the variable is declaredwithout generics information, the type checker knows what isthe component type. Therefore, such code would fail compilation:
@groovy.transform.TypeCheckedvoid flowTypingTypeConstraints1() { def list = ['a','b','c'](1) list.add(1)(2)}@groovy.transform.TypeCheckedvoid flowTypingTypeConstraints2() { List<?> list = [](3) list.addAll(['a','b','c'])(4) list.add(1)(5)}| 1 | list is inferred asArrayList<String> |
| 2 | so adding anint to aArrayList<String> is a compile-time error |
| 3 | list is declared asList<?> |
| 4 | the inferred type oflist here isList<capture-of ?>, so callingaddAll with a list of anything is a compile-time error |
| 5 | and callingadd with anint is also a compile-time error for the same reason; onlyadd(null) is allowed |
Fixing this requires adding an explicit, non-wildcard type argument:
@groovy.transform.TypeCheckedvoid flowTypingTypeConstraints3() { List<Serializable> list = [](1) list.addAll(['a','b','c'])(2) list.add(1)(3)}| 1 | list is declared asList<Serializable> and initialized with an empty list |
| 2 | elements added to the list conform to the declaration type of the list |
| 3 | and adding an integer is allowed |
Flow typing has been introduced to reduce the difference in semantics between classic and static Groovy. In particular,consider the behavior of this code in Java:
public Integer compute(String str) { return str.length();}public String compute(Object o) { return "Nope";}// ...Object string = "Some string";(1)Object result = compute(string);(2)System.out.println(result);(3)| 1 | o is declared as anObject and assigned aString |
| 2 | we call thecompute method witho |
| 3 | and print the result |
In Java, this code will outputNope, because method selection is done at compile time and based on thedeclared types.So even ifo is aString at runtime, it is still theObject version which is called, becauseo has been declaredas anObject. To be short, in Java, declared types are most important, be it variable types, parameter types or returntypes.
In Groovy, we could write:
int compute(String string) { string.length() }String compute(Object o) { "Nope" }Object o = 'string'def result = compute(o)println resultBut this time, it will return6, because the method which is chosenat runtime, based on theactualargument types. So at runtime,o is aString so theString variant is used. Note that this behavior has nothingto do with type checking, it’s the way Groovy works in general: dynamic dispatch.
In type checked Groovy, we want to make sure the type checker selects the same methodat compile time, that the runtimewould choose. It is not possible in general, due to the semantics of the language, but we can make things better with flowtyping. With flow typing,o isinferred as aString when thecompute method is called, so the version which takesaString and returns anint is chosen. This means that we can infer the return type of the method to be anint, andnot aString. This is important for subsequent calls and type safety.
So in type checked Groovy, flow typing is a very important concept, which also implies that if@TypeChecked is applied,methods are selected based on theinferred types of the arguments, not on the declared types. This doesn’t ensure 100%type safety, because the type checkermay select a wrong method, but it ensures the closest semantics to dynamic Groovy.
A combination offlow typing andleast upper bound inference is used to performadvanced type inference and ensure type safety in multiple situations. In particular, program control structures arelikely to alter the inferred type of a variable:
class Top { void methodFromTop() {}}class Bottom extends Top { void methodFromBottom() {}}def oif (someCondition) { o = new Top()(1)} else { o = new Bottom()(2)}o.methodFromTop()(3)o.methodFromBottom() // compilation error(4)| 1 | ifsomeCondition is true,o is assigned aTop |
| 2 | ifsomeCondition is false,o is assigned aBottom |
| 3 | callingmethodFromTop is safe |
| 4 | but callingmethodFromBottom is not, so it’s a compile time error |
When the type checker visits anif/else control structure, it checks all variables which are assigned inif/else branchesand computes theleast upper bound of all assignments. This type is the type of the inferred variableafter theif/else block, so in this example,o is assigned aTop in theif branch and aBottom in theelsebranch. TheLUB of those is aTop, so after the conditional branches, the compiler inferso as beingaTop. CallingmethodFromTop will therefore be allowed, but notmethodFromBottom.
The same reasoning exists with closures and in particular closure shared variables. A closure shared variable is a variablewhich is defined outside of a closure, but used inside a closure, as in this example:
def text = 'Hello, world!'(1)def closure = { println text(2)}| 1 | a variable namedtext is declared |
| 2 | text is used from inside a closure. It is aclosure shared variable. |
Groovy allows developers to use those variables without requiring them to be final. This means that a closure sharedvariable can be reassigned inside a closure:
String resultdoSomething { String it -> result = "Result: $it"}result = result?.toUpperCase()The problem is that a closure is an independent block of code that can be executed (or not) atany time. In particular,doSomething may be asynchronous, for example. This means that the body of a closure doesn’t belong to the main controlflow. For that reason, the type checker also computes, for each closure shared variable, theLUB of allassignments of the variable, and will use thatLUB as the inferred type outside of the scope of the closure, like inthis example:
class Top { void methodFromTop() {}}class Bottom extends Top { void methodFromBottom() {}}def o = new Top()(1)Thread.start { o = new Bottom()(2)}o.methodFromTop()(3)o.methodFromBottom() // compilation error(4)| 1 | a closure-shared variable is first assigned aTop |
| 2 | inside the closure, it is assigned aBottom |
| 3 | methodFromTop is allowed |
| 4 | methodFromBottom is a compilation error |
Here, it is clear that whenmethodFromBottom is called, there’s no guarantee, at compile-time or runtime that thetype ofo willeffectively be aBottom. There are chances that it will be, but we can’t make sure, because it’sasynchronous. So the type checker will only allow calls on theleast upper bound, which is here aTop.
The type checker performs special inference on closures, resulting on additional checks on one side and improved fluencyon the other side.
The first thing that the type checker is capable of doing is inferring thereturn type of a closure. This is simply illustrated in the following example:
@groovy.transform.TypeCheckedint testClosureReturnTypeInference(String arg) { def cl = { "Arg: $arg" }(1) def val = cl()(2) val.length()(3)}| 1 | a closure is defined, and it returns a string (more precisely aGString) |
| 2 | we call the closure and assign the result to a variable |
| 3 | the type checker inferred that the closure would return a string, so callinglength() is allowed |
As you can see, unlike a method which declares its return type explicitly, there’s no need to declare the return typeof a closure: its type is inferred from the body of the closure.
It’s worth noting that return type inference is only applicable to closures. While the type checker could do thesame on a method, it is in practice not desirable:in general, methods can be overridden and it is not staticallypossible to make sure that the method which is called is not an overridden version. So flow typing would actuallythink that a method returns something, while in reality, it could return something else, like illustrated in thefollowing example:
@TypeCheckedclass A { def compute() { 'some string' }(1) def computeFully() { compute().toUpperCase()(2) }}@TypeCheckedclass B extends A { def compute() { 123 }(3)}| 1 | classA defines a methodcompute which effectively returns aString |
| 2 | this will fail compilation because the return type ofcompute isdef(akaObject) |
| 3 | classB extendsA and redefinescompute, this type returning anint |
As you can see, if the type checker relied on the inferred return type of a method, withflow typing,the type checker could determine that it is ok to calltoUpperCase. It is in fact anerror, because a subclass canoverridecompute and return a different object. Here,B#compute returns anint, so someone callingcomputeFullyon an instance ofB would see a runtime error. The compiler prevents this from happening by using the declared returntype of methods instead of the inferred return type.
For consistency, this behavior is the same forevery method, even if they are static or final.
In addition to the return type, it is possible for a closure to infer its parameter types from the context. There aretwo ways for the compiler to infer the parameter types:
throughimplicit SAM type coercion
through API metadata
To illustrate this, lets start with an example that will fail compilation due to the inability for the type checkerto infer the parameter types:
class Person { String name int age}void inviteIf(Person p, Closure<Boolean> predicate) {(1) if (predicate.call(p)) { // send invite // ... }}@groovy.transform.TypeCheckedvoid failCompilation() { Person p = new Person(name: 'Gerard', age: 55) inviteIf(p) {(2) it.age >= 18 // No such property: age(3) }}| 1 | theinviteIf method accepts aPerson and aClosure |
| 2 | we call it with aPerson and aClosure |
| 3 | yetit is not statically known as being aPerson and compilation fails |
In this example, the closure body containsit.age. With dynamic, not type checked code, this would work, because thetype ofit would be aPerson at runtime. Unfortunately, at compile-time, there’s no way to know what is the typeofit, just by reading the signature ofinviteIf.
To be short, the type checker doesn’t have enough contextual information on theinviteIf method to determine staticallythe type ofit. This means that the method call needs to be rewritten like this:
inviteIf(p) { Person it ->(1) it.age >= 18}| 1 | the type ofit needs to be declared explicitly |
By explicitly declaring the type of theit variable, you can work around the problem and make this code staticallychecked.
For an API or framework designer, there are two ways to make this more elegant for users, so that they don’t have todeclare an explicit type for the closure parameters. The first one, and easiest, is to replace the closure with aSAM type:
interface Predicate<On> { boolean apply(On e) }(1)void inviteIf(Person p, Predicate<Person> predicate) {(2) if (predicate.apply(p)) { // send invite // ... }}@groovy.transform.TypeCheckedvoid passesCompilation() { Person p = new Person(name: 'Gerard', age: 55) inviteIf(p) {(3) it.age >= 18(4) }}| 1 | declare aSAM interface with anapply method |
| 2 | inviteIf now uses aPredicate<Person> instead of aClosure<Boolean> |
| 3 | there’s no need to declare the type of theit variable anymore |
| 4 | it.age compiles properly, the type ofit is inferred from thePredicate#apply method signature |
| By using this technique, we leverage theautomatic coercion of closures to SAM types feature of Groovy.Whether you should use aSAM type or aClosure really depends on what you need to do. In a lot of cases,using a SAM interface is enough, especially if you consider functional interfaces as they are found in Java 8. However,closures provide features that are not accessible to functional interfaces. In particular, closures can have a delegate,and owner and can be manipulated as objects (for example, cloned, serialized, curried, …) before being called. They canalso support multiple signatures (polymorphism). So if you need that kind of manipulation, it is preferable to switch tothe most advanced type inference annotations which are described below. |
The original issue that needs to be solved when it comes to closure parameter type inference, that is to say, staticallydetermining the types of the arguments of a closurewithout having to have them explicitly declared, is that the Groovytype system inherits the Java type system, which is insufficient to describe the types of the arguments.
@ClosureParams annotationGroovy provides an annotation,@ClosureParams which is aimed at completing type information. This annotation is primarilyaimed at framework and API developers who want to extend the capabilities of the type checker by providing type inferencemetadata. This is important if your library makes use of closures and that you want the maximum level of tooling supporttoo.
Let’s illustrate this by fixing the original example, introducing the@ClosureParams annotation:
import groovy.transform.stc.ClosureParamsimport groovy.transform.stc.FirstParamvoid inviteIf(Person p, @ClosureParams(FirstParam) Closure<Boolean> predicate) {(1) if (predicate.call(p)) { // send invite // ... }}inviteIf(p) {(2) it.age >= 18}| 1 | the closure parameter is annotated with@ClosureParams |
| 2 | it’s not necessary to use an explicit type forit, which is inferred |
The@ClosureParams annotation minimally accepts one argument, which is named atype hint. A type hint is a class whichis responsible for completing type information at compile time for the closure. In this example, the type hint being usedisgroovy.transform.stc.FirstParam which indicated to the type checker that the closure will accept one parameterwhose type is the type of the first parameter of the method. In this case, the first parameter of the method isPerson,so it indicates to the type checker that the first parameter of the closure is in fact aPerson.
A second optional argument is namedoptions. Its semantics depend on thetype hint class. Groovy comes withvarious bundled type hints, illustrated in the table below:
| Type hint | Polymorphic? | Description and examples |
|---|---|---|
| No | The first (resp. second, third) parameter type of the method |
| No | The first generic type of the first (resp. second, third) parameter of the method Variants for |
| No | A type hint for which the type of closure parameters comes from the options string. This type hint supports asingle signature and each of the parameter is specified as a value of theoptions arrayusing a fully-qualified type name or a primitive type. |
| Yes | A dedicated type hint for closures that either work on a This type hintrequires that the first argument is a |
| Yes | Infers closure parameter types from the abstract method of some type. A signature is inferred foreach abstract method. If there are multiple signatures like in the example above, the type checker willonly be able to infer the types ofthe arguments if the arity of each method is different. In the example above, |
| Yes | Infers the closure parameter types from the A single signature for a closure accepting a A polymorphic closure, accepting either a A polymorphic closure, accepting either a |
Even though you useFirstParam,SecondParam orThirdParam as a type hint, it doesn’t strictly mean that theargument which will be passed to the closurewill be the first (resp. second, third) argument of the method call. Itonly means that thetype of the parameter of the closure will be thesame as the type of the first (resp. second, third) argument of the method call. |
In short, the lack of the@ClosureParams annotation on a method accepting aClosure willnot fail compilation. If present (and it can be present in Java sources as well as Groovy sources), then the type checker hasmore information and can perform additional type inference. This makes this feature particularly interesting for framework developers.
A third optional argument is namedconflictResolutionStrategy. It can reference a class (extending fromClosureSignatureConflictResolver) that can perform additional resolution of parameter types if more thanone are found after initial inference calculations are complete. Groovy comes with a default type resolverwhich does nothing, and another which selects the first signature if multiple are found. The resolver isonly invoked if more than one signature is found and is by design a post processor. Any statements which needinjected typing information must pass one of the parameter signatures determined through type hints. Theresolver then picks among the returned candidate signatures.
@DelegatesToThe@DelegatesTo annotation is used by the type checker to infer the type of the delegate. It allows the API designerto instruct the compiler what is the type of the delegate and the delegation strategy. The@DelegatesTo annotation isdiscussed in aspecific section.
In thetype checking section, we have seen that Groovy provides optional type checking thanks to the@TypeChecked annotation. The type checker runs at compile time and performs a static analysis of dynamic code. Theprogram will behave exactly the same whether type checking has been enabled or not. This means that the@TypeCheckedannotation is neutral in regard to the semantics of a program. Even though it may be necessary to add type informationin the sources so that the program is considered type safe, in the end, the semantics of the program are the same.
While this may sound fine, there is actually one issue with this: type checking of dynamic code, done at compile time, isby definition only correct if no runtime specific behavior occurs. For example, the following program passes type checking:
class Computer { int compute(String str) { str.length() } String compute(int x) { String.valueOf(x) }}@groovy.transform.TypeCheckedvoid test() { def computer = new Computer() computer.with { assert compute(compute('foobar')) =='6' }}There are twocompute methods. One accepts aString and returns anint, the other accepts anint and returnsaString. If you compile this, it is considered type safe: the innercompute('foobar') call will return anint,and callingcompute on thisint will in turn return aString.
Now, before callingtest(), consider adding the following line:
Computer.metaClass.compute = { String str -> new Date() }Using runtime metaprogramming, we’re actually modifying the behavior of thecompute(String) method, so that instead ofreturning the length of the provided argument, it will return aDate. If you execute the program, it will fail atruntime. Since this line can be added from anywhere, in any thread, there’s absolutely no way for the type checker tostatically make sure that no such thing happens. In short, the type checker is vulnerable to monkey patching. This isjust one example, but this illustrates the concept that doing static analysis of a dynamic program is inherently wrong.
The Groovy language provides an alternative annotation to@TypeChecked which will actually make sure that the methodswhich are inferred as being calledwill effectively be called at runtime. This annotation turns the Groovy compilerinto astatic compiler, where all method calls are resolved at compile timeand the generated bytecode makes surethat this happens: the annotation is@groovy.transform.CompileStatic.
@CompileStatic annotationThe@CompileStatic annotation can be added anywhere the@TypeChecked annotation can be used, that is to say ona class or a method. It is not necessary to add both@TypeChecked and@CompileStatic, as@CompileStatic performseverything@TypeChecked does, but in addition triggers static compilation.
Let’s take theexample which failed, but this time let’s replace the@TypeChecked annotationwith@CompileStatic:
class Computer { int compute(String str) { str.length() } String compute(int x) { String.valueOf(x) }}@groovy.transform.CompileStaticvoid test() { def computer = new Computer() computer.with { assert compute(compute('foobar')) =='6' }}Computer.metaClass.compute = { String str -> new Date() }test()This is theonly difference. If we execute this program, this time, there is no runtime error. Thetest methodbecame immune to monkey patching, because thecompute methods which are called in its body are linked at compiletime, so even if the metaclass ofComputer changes, the program still behavesas expected by the type checker.
There are several benefits of using@CompileStatic on your code:
type safety
immunity tomonkey patching
performance improvements
The performance improvements depend on the kind of program you are executing. If it is I/O bound, the difference betweenstatically compiled code and dynamic code is barely noticeable. On highly CPU intensive code, since the bytecode whichis generated is very close, if not equal, to the one that Java would produce for an equivalent program, the performanceis greatly improved.
| Using theinvokedynamic version of Groovy, which is accessible to people using JDK 7 and above, the performanceof the dynamic code should be very close to the performance of statically compiled code. Sometimes, it can even be faster!There is only one way to determine which version you should choose: measuring. The reason is that depending on your programand the JVM that you use, the performance can be significantly different. In particular, theinvokedynamic version ofGroovy is very sensitive to the JVM version in use. |
Despite being a dynamic language, Groovy can be used with astatic type checkerat compile time, enabled using the@TypeChecked annotation. In this mode, thecompiler becomes more verbose and throws errors for, example, typos, non-existentmethods, etc. This comes with a few limitations though, most of them coming fromthe fact that Groovy remains inherently a dynamic language. For example, youwouldn’t be able to use type checking on code that uses the markup builder:
def builder = new MarkupBuilder(out)builder.html { head { // ... } body { p 'Hello, world!' }}In the previous example, none of thehtml,head,body orp methodsexist. However if you execute the code, it works because Groovy uses dynamic dispatchand converts those method calls at runtime. In this builder, there’s no limitation aboutthe number of tags that you can use, nor the attributes, which means there is no chancefor a type checker to know about all the possible methods (tags) at compile time, unlessyou create a builder dedicated to HTML for example.
Groovy is a platform of choice when it comes to implement internal DSLs. The flexible syntax,combined with runtime and compile-time metaprogramming capabilities make Groovy an interestingchoice because it allows the programmer to focus on the DSL rather thanon tooling or implementation. Since Groovy DSLs are Groovy code, it’seasy to have IDE support without having to write a dedicated plugin forexample.
In a lot of cases, DSL engines are written in Groovy (or Java) then usercode is executed as scripts, meaning that you have some kind of wrapperon top of user logic. The wrapper may consist, for example, in aGroovyShell orGroovyScriptEngine that performs some tasks transparentlybefore running the script (adding imports, applying AST transforms,extending a base script,…). Often, user written scripts come toproduction without testing because the DSL logic comes to a pointwhere any user may write code using the DSL syntax. In the end, a usermay just ignore that what they write is actually code. This adds somechallenges for the DSL implementer, such as securing execution of usercode or, in this case, early reporting of errors.
For example, imagine a DSL which goal is to drive a rover on Marsremotely. Sending a message to the rover takes around 15 minutes. If therover executes the script and fails with an error (say a typo), you havetwo problems:
first, feedback comes only after 30 minutes (the time needed for therover to get the script and the time needed to receive the error)
second, some portion of the script has been executed and you may haveto change the fixed script significantly (implying that you need to knowthe current state of the rover…)
Type checking extensions is a mechanism that willallow the developer of a DSL engine to make those scripts safer byapplying the same kind of checks that static type checking allows onregular groovy classes.
The principle, here, is to fail early, that isto say fail compilation of scripts as soon as possible, and if possibleprovide feedback to the user (including nice error messages).
In short, the idea behind type checking extensions is to make the compileraware of all the runtime metaprogramming tricks that the DSL uses, so thatscripts can benefit the same level of compile-time checks as a verbose staticallycompiled code would have. We will see that you can go even further by performingchecks that a normal type checker wouldn’t do, delivering powerful compile-timechecks for your users.
The @TypeChecked annotation supports an attributenamed extensions. This parameter takes an array of stringscorresponding to a list oftype checking extensions scripts. Thosescripts are found at compile time on classpath. For example, you wouldwrite:
@TypeChecked(extensions='/path/to/myextension.groovy')void foo() { ...}In that case, the foo methods would be type checked with the rules ofthe normal type checker completed by those found inthe myextension.groovy script. Note that while internally the typechecker supports multiple mechanisms to implement type checkingextensions (including plain old java code), the recommended way is touse those type checking extension scripts.
The idea behind type checking extensions is to use a DSL to extend thetype checker capabilities. This DSL allows you to hook into thecompilation process, more specifically the type checking phase, using an"event-driven" API. For example, when the type checker enters a methodbody, it throws a beforeVisitMethod event that the extension can react to:
beforeVisitMethod { methodNode -> println "Entering ${methodNode.name}"}Imagine that you have this rover DSL at hand. A user would write:
robot.move 100If you have a class defined as such:
class Robot { Robot move(int qt) { this }}The script can be type checked before being executed using the followingscript:
def config = new CompilerConfiguration()config.addCompilationCustomizers( new ASTTransformationCustomizer(TypeChecked)(1))def shell = new GroovyShell(config)(2)def robot = new Robot()shell.setVariable('robot', robot)shell.evaluate(script)(3)| 1 | a compiler configuration adds the@TypeChecked annotation to all classes |
| 2 | use the configuration in aGroovyShell |
| 3 | so that scripts compiled using the shell are compiled with@TypeChecked without the user having to add it explicitly |
Using the compiler configuration above, we can apply @TypeCheckedtransparently to the script. In that case, it will fail at compiletime:
[Static type checking] - The variable [robot] is undeclared.
Now, we will slightly update the configuration to include the``extensions'' parameter:
config.addCompilationCustomizers( new ASTTransformationCustomizer( TypeChecked, extensions:['robotextension.groovy']))Then add the following to your classpath:
unresolvedVariable { var -> if ('robot'==var.name) { storeType(var, classNodeFor(Robot)) handled = true }}Here, we’re telling the compiler that if anunresolved variable is foundand that the name of the variable is robot, then we can make sure that the type of thisvariable isRobot.
The type checking API is a low level API, dealing with the AbstractSyntax Tree. You will have to know your AST well to develop extensions,even if the DSL makes it much easier than just dealing with AST codefrom plain Java or Groovy.
The type checker sends the following events, to which an extensionscript can react:
Event name | setup |
Called When | Called after the type checker finished initialization |
Arguments | none |
Usage | Can be used to perform setup of your extension |
Event name | finish |
Called When | Called after the type checker completed type checking |
Arguments | none |
Usage | Can be used to perform additional checks after the type checker has finished its job. |
Event name | unresolvedVariable |
Called When | Called when the type checker finds an unresolved variable |
Arguments | VariableExpression vexp |
Usage | Allows the developer to help the type checker with user-injected variables. |
Event name | unresolvedProperty |
Called When | Called when the type checker cannot find a property on the receiver |
Arguments | PropertyExpression pexp |
Usage | Allows the developer to handle "dynamic" properties |
Event name | unresolvedAttribute |
Called When | Called when the type checker cannot find an attribute on the receiver |
Arguments | AttributeExpression aexp |
Usage | Allows the developer to handle missing attributes |
Event name | beforeMethodCall |
Called When | Called before the type checker starts type checking a method call |
Arguments | MethodCall call |
Usage | Allows you to intercept method calls before thetype checker performs its own checks. This is useful if you want toreplace the default type checking with a custom one for a limited scope.In that case, you must set the handled flag to true, so that the typechecker skips its own checks. |
Event name | afterMethodCall |
Called When | Called once the type checker has finished type checking a method call |
Arguments | MethodCall call |
Usage | Allow you to perform additional checks after the typechecker has done its own checks. This is in particular useful if youwant to perform the standard type checking tests but also want to ensureadditional type safety, for example checking the arguments against eachother.Note that |
Event name | onMethodSelection |
Called When | Called by the type checker when it finds a method appropriate for a method call |
Arguments | Expression expr, MethodNode node |
Usage | The type checker works by inferringargument types of a method call, then chooses a target method. If itfinds one that corresponds, then it triggers this event. It is forexample interesting if you want to react on a specific method call, suchas entering the scope of a method that takes a closure as argument (asin builders).Please note that this event may be thrown for various typesof expressions, not only method calls (binary expressions for example). |
Event name | methodNotFound |
Called When | Called by the type checker when it fails to find an appropriate method for a method call |
Arguments | ClassNode receiver, String name, ArgumentListExpression argList, ClassNode[] argTypes,MethodCall call |
Usage | Unlike |
Event name | beforeVisitMethod |
Called When | Called by the type checker before type checking a method body |
Arguments | MethodNode node |
Usage | The type checker will call this method beforestarting to type check a method body. If you want, for example, toperform type checking by yourself instead of letting the type checker doit, you have to set the handled flag to true. This event can also be usedto help define the scope of your extension (for example, applying itonly if you are inside method foo). |
Event name | afterVisitMethod |
Called When | Called by the type checker after type checking a method body |
Arguments | MethodNode node |
Usage | Gives you the opportunity to perform additionalchecks after a method body is visited by the type checker. This isuseful if you collect information, for example, and want to performadditional checks once everything has been collected. |
Event name | beforeVisitClass |
Called When | Called by the type checker before type checking a class |
Arguments | ClassNode node |
Usage | If a class is type checked, thenbefore visiting the class, this event will be sent. It is also the casefor inner classes defined inside a class annotated with |
Event name | afterVisitClass |
Called When | Called by the type checker after having finished the visit of a type checked class |
Arguments | ClassNode node |
Usage | Calledfor every class being type checked after the type checker finished itswork. This includes classes annotated with |
Event name | incompatibleAssignment |
Called When | Called when the type checker thinks that an assignment is incorrect, meaning that the right-hand side of an assignment is incompatible with the left-hand side |
Arguments | ClassNode lhsType, ClassNode rhsType, Expression assignment |
Usage | Gives thedeveloper the ability to handle incorrect assignments. This is forexample useful if a class overrides |
Event name | incompatibleReturnType |
Called When | Called when the type checker thinks that a return value is incompatibe with the return type of the enclosing closure or method |
Arguments | ReturnStatement statement, ClassNode valueType |
Usage | Gives the developer the ability to handle incorrect return values. This is forexample useful when the return value will undergo implicit conversion or theenclosing closure’s target type is difficult to infer properly. In that case,you can help the type checker just by telling it that the assignment is valid(by setting the |
Event name | ambiguousMethods |
Called When | Called when the type checker cannot choose between several candidate methods |
Arguments | List<MethodNode> methods, Expression origin |
Usage | Gives thedeveloper the ability to handle incorrect assignments. This is forexample useful if a class overrides |
Of course, an extension script may consist of several blocks, and youcan have multiple blocks responding to the same event. This makes theDSL look nicer and easier to write. However, reacting to events is farfrom sufficient. If you know you can react to events, you also need todeal with the errors, which implies severalhelper methods that willmake things easier.
The DSL relies on a support class called org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport .This class itself extends org.codehaus.groovy.transform.stc.TypeCheckingExtension .Those two classes define a number ofhelper methods that will make workingwith the AST easier, especially regarding type checking. One interestingthing to know is that you have access to the type checker. This meansthat you can programmatically call methods of the type checker,including those that allow you tothrow compilation errors.
The extension script delegates to the org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport class,meaning that you have direct access to the following variables:
context: the type checker context, of type org.codehaus.groovy.transform.stc.TypeCheckingContext
typeCheckingVisitor: the type checker itself, a org.codehaus.groovy.transform.stc.StaticTypeCheckingVisitor instance
generatedMethods: a list of "generated methods", which is in fact the list of "dummy" methods that you can createinside a type checking extension using the newMethod calls
The type checking context contains a lot of information that is usefulin context for the type checker. For example, the current stack ofenclosing method calls, binary expressions, closures, … This informationis in particular important if you have to knowwhere you are when anerror occurs and that you want to handle it.
In addition to facilities provided byGroovyTypeCheckingExtensionSupport andStaticTypeCheckingVisitor,a type-checking DSL script imports static members fromorg.codehaus.groovy.ast.ClassHelper andorg.codehaus.groovy.transform.stc.StaticTypeCheckingSupport granting access to common types viaOBJECT_TYPE,STRING_TYPE,THROWABLE_TYPE, etc. and checks likemissesGenericsTypes(ClassNode),isClassClassNodeWrappingConcreteType(ClassNode) and so on.
Handling class nodes is something that needs particular attention whenyou work with a type checking extension. Compilation works with anabstract syntax tree (AST) and the tree may not be complete when you aretype checking a class. This also means that when you refer to types, youmust not use class literals such as String or HashSet, but to classnodes representing those types. This requires a certain level ofabstraction and understanding how Groovy deals with class nodes. To makethings easier, Groovy supplies several helper methods to deal with classnodes. For example, if you want to say "the type for String", you canwrite:
assert classNodeFor(String) instanceof ClassNodeYou would also note that there is a variant of classNodeFor that takesa String as an argument, instead of a Class. In general, youshould not use that one, because it would create a class node forwhich the name isString, but without any method, any property, …defined on it. The first version returns a class node that is resolvedbut the second one returns one that is not. So the latter should bereserved for very special cases.
The second problem that you might encounter is referencing a type whichis not yet compiled. This may happen more often than you think. Forexample, when you compile a set of files together. In that case, if youwant to say "that variable is of type Foo" butFoo is not yetcompiled, you can still refer to theFoo class nodeusing lookupClassNodeFor:
assert lookupClassNodeFor('Foo') instanceof ClassNodeSay that you know that variable foo is of type Foo and you want totell the type checker about it. Then you can use the storeType method,which takes two arguments: the first one is the node for which you wantto store the type and the second one is the type of the node. If youlook at the implementation of storeType, you would see that itdelegates to the type checker equivalent method, which itself does a lotof work to store node metadata. You would also see that storing the typeis not limited to variables: you can set the type of any expression.
Likewise, getting the type of an AST node is just a matter ofcalling getType on that node. This would in general be what you want,but there’s something that you must understand:
getType returns the inferred type of an expression. This meansthat it will not return, for a variable declared of type Object theclass node for Object, but the inferred type of this variable at thispoint of the code (flow typing)
if you want to access the origin type of a variable (orfield/parameter), then you must call the appropriate method on the ASTnode
To throw a type checking error, you only have to call theaddStaticTypeError method which takes two arguments:
a message which is a string that will be displayed to the end user
anAST node responsible for the error. It’s better to provide the bestsuiting AST node because it will be used to retrieve the line and columnnumbers
It is often required to know the type of an AST node. For readability,the DSL provides a special isXXXExpression method that will delegate tox instance of XXXExpression. For example, instead of writing:
if (node instanceof BinaryExpression) { ...}you can just write:
if (isBinaryExpression(node)) { ...}When you perform type checking of dynamic code, you may often face thecase when you know that a method call is valid but there is no "real"method behind it. As an example, take the Grails dynamic finders. Youcan have a method call consisting of a method named findByName(…). Asthere’s no findByName method defined in the bean, the type checkerwould complain. Yet, you would know that this method wouldn’t fail atruntime, and you can even tell what is the return type of this method.For this case, the DSL supports two special constructs that consist ofphantom methods. This means that you will return a method node thatdoesn’t really exist but is defined in the context of type checking.Three methods exist:
newMethod(String name, Class returnType)
newMethod(String name, ClassNode returnType)
newMethod(String name, Callable<ClassNode> return Type)
All three variants do the same: they create a new method node which nameis the supplied name and define the return type of this method.Moreover, the type checker would add those methods inthe generatedMethods list (see isGenerated below). The reason why weonly set a name and a return type is that it is only what you need in90% of the cases. For example, in the findByName example upper, theonly thing you need to know is that findByName wouldn’t fail atruntime, and that it returns a domain class. The Callable version ofreturn type is interesting because it defers the computation of thereturn type when the type checker actually needs it. This is interestingbecause in some circumstances, you may not know the actual return typewhen the type checker demands it, so you can use a closure that will becalled each time getReturnType is called by the type checker on thismethod node. If you combine this with deferred checks, you can achievepretty complex type checking including handling of forward references.
newMethod(name) { // each time getReturnType on this method node will be called, this closure will be called! println 'Type checker called me!' lookupClassNodeFor(Foo) // return type}Should you need more than the name and return type, you can alwayscreate a new MethodNode by yourself.
Scoping is very important in DSL type checking and is one of the reasonswhy we couldn’t use a pointcut based approach to DSL type checking.Basically, you must be able to define very precisely when your extensionapplies and when it does not. Moreover, you must be able to handlesituations that a regular type checker would not be able to handle, suchas forward references:
point a(1,1)line a,b // b is referenced afterwards!point b(5,2)Say for example that you want to handle a builder:
builder.foo { bar baz(bar)}Your extension, then, should only be active once you’ve enteredthe foo method, and inactive outside this scope. But you could havecomplex situations like multiple builders in the same file or embeddedbuilders (builders in builders). While you should not try to fix allthis from start (you must accept limitations to type checking), the typechecker does offer a nice mechanism to handle this: a scoping stack,using the newScope and scopeExit methods.
newScope creates a new scope and puts it on top of the stack
scopeExits pops a scope from the stack
A scope consists of:
a parent scope
a map of custom data
If you want to look at the implementation, it’s simply aLinkedHashMap(org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport.TypeCheckingScope),but it’s quite powerful. For example, you can use such a scope to storea list of closures to be executed when you exit the scope. This is howyou would handle forward references:
def scope = newScope()scope.secondPassChecks = []//...scope.secondPassChecks << { println 'executed later' }// ...scopeExit { secondPassChecks*.run() // execute deferred checks}That is to say, that if at some point you are not able to determine thetype of an expression, or that you are not able to check at this pointthat an assignment is valid or not, you can still make the check later…This is a very powerful feature. Now, newScope and scopeExitprovide some interesting syntactic sugar:
newScope { secondPassChecks = []}At anytime in the DSL, you can access the current scopeusing getCurrentScope() or more simply currentScope:
//...currentScope.secondPassChecks << { println 'executed later' }// ...The general schema would then be:
determine apointcut where you push a new scope on stack andinitialize custom variables within this scope
using the various events, you can use the information stored in yourcustom scope to perform checks, defer checks,…
determine apointcut where you exit the scope, call scopeExitand eventually perform additional checks
For the complete list of helper methods, please refer tothe org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport and org.codehaus.groovy.transform.stc.TypeCheckingExtension classes. However,take special attention to those methods:
isDynamic: takes a VariableExpression as argument and returns trueif the variable is a DynamicExpression, which means, in a script, thatit wasn’t defined using a type ordef.
isGenerated: takes a MethodNode as an argument and tells if themethod is one that was generated by the type checker extension usingthe newMethod method
isAnnotatedBy: takes an AST node and a Class (or ClassNode), andtells if the node is annotated with this class. For example:isAnnotatedBy(node, NotNull)
getTargetMethod: takes a method call as argument and returnsthe MethodNode that the type checker has determined for it
delegatesTo: emulates the behaviour of the @DelegatesToannotation. It allows you to tell that the argument will delegate to aspecific type (you can also specify the delegation strategy)
All the examples above use type checking scripts. They are found in source form in classpath, meaning that:
a Groovy source file, corresponding to the type checking extension, is available on compilation classpath
this file is compiled by the Groovy compiler for each source unit being compiled (often, a source unit correspondsto a single file)
It is a very convenient way to develop type checking extensions, however it implies a slower compilation phase, becauseof the compilation of the extension itself for each file being compiled. For those reasons, it can be practical to relyon a precompiled extension. You have two options to do this:
write the extension in Groovy, compile it, then use a reference to the extension class instead of the source
write the extension in Java, compile it, then use a reference to the extension class
Writing a type checking extension in Groovy is the easiest path. Basically, the idea is that the type checking extensionscript becomes the body of the main method of a type checking extension class, as illustrated here:
import org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupportclass PrecompiledExtension extends GroovyTypeCheckingExtensionSupport.TypeCheckingDSL {(1) @Override Object run() {(2) unresolvedVariable { var -> if ('robot'==var.name) { storeType(var, classNodeFor(Robot))(3) handled = true } } }}| 1 | extending theTypeCheckingDSL class is the easiest |
| 2 | then the extension code needs to go inside therun method |
| 3 | and you can use the very same events as an extension written in source form |
Setting up the extension is very similar to using a source form extension:
config.addCompilationCustomizers( new ASTTransformationCustomizer( TypeChecked, extensions:['typing.PrecompiledExtension']))The difference is that instead of using a path in classpath, you just specify the fully qualified class name of theprecompiled extension.
In case you really want to write an extension in Java, then you will not benefit from the type checking extension DSL.The extension above can be rewritten in Java this way:
import org.codehaus.groovy.ast.ClassHelper;import org.codehaus.groovy.ast.expr.VariableExpression;import org.codehaus.groovy.transform.stc.AbstractTypeCheckingExtension;import org.codehaus.groovy.transform.stc.StaticTypeCheckingVisitor;public class PrecompiledJavaExtension extends AbstractTypeCheckingExtension {(1) public PrecompiledJavaExtension(final StaticTypeCheckingVisitor typeCheckingVisitor) { super(typeCheckingVisitor); } @Override public boolean handleUnresolvedVariableExpression(final VariableExpression vexp) {(2) if ("robot".equals(vexp.getName())) { storeType(vexp, ClassHelper.make(Robot.class)); setHandled(true); return true; } return false; }}| 1 | extend theAbstractTypeCheckingExtension class |
| 2 | then override thehandleXXX methods as required |
It is totally possible to use the@Grab annotation in a type checking extension.This means you can include libraries that would only beavailable at compile time. In that case, you must understand that youwould increase the time of compilation significantly (at least, thefirst time it grabs the dependencies).
A type checking extension is just a script that need to be on classpath. As such,you can share it as is, or bundle it in a jar file that would be added to classpath.
While you can configure the compiler to transparently add type checking extensions to yourscript, there is currently no way to apply an extension transparently just by having it onclasspath.
Type checking extensions are used with@TypeChecked but can also be used with@CompileStatic. However, you mustbe aware that:
a type checking extension used with@CompileStatic will in general not be sufficient to let the compiler know howto generate statically compilable code from "unsafe" code
it is possible to use a type checking extension with@CompileStatic just to enhance type checking, that is to sayintroducemore compilation errors, without actually dealing with dynamic code
Let’s explain the first point, which is that even if you use an extension, the compiler will not know how to compileyour code statically: technically, even if you tell the type checker what is the type of a dynamicvariable, for example, it would not know how to compile it. Is itgetBinding('foo'),getProperty('foo'),delegate.getFoo(),…? There’s absolutely no direct way to tell the static compiler how to compile suchcode even if you use a type checking extension (that would, again, only give hints about the type).
One possible solution for this particular example is to instruct the compiler to usemixed mode compilation.The more advanced one is to useAST transformations during type checking but it is far morecomplex.
Type checking extensions allow you to help the type checker where itfails, but it also allows you to fail where it doesn’t. In that context,it makes sense to support extensions for @CompileStatic too. Imaginean extension that is capable of type checking SQL queries. In that case,the extension would be valid in both dynamic and static context, becausewithout the extension, the code would still pass.
In the previous section, we highlighted the fact that you can activate type checking extensions with@CompileStatic. In that context, the type checker would not complain anymore about some unresolved variables orunknown method calls, but it would still wouldn’t know how to compile them statically.
Mixed mode compilation offers a third way, which is to instruct the compiler that whenever an unresolved variableor method call is found, then it should fall back to a dynamic mode. This is possible thanks to type checking extensionsand a specialmakeDynamic call.
To illustrate this, let’s come back to theRobot example:
robot.move 100And let’s try to activate our type checking extension using@CompileStatic instead of@TypeChecked:
def config = new CompilerConfiguration()config.addCompilationCustomizers( new ASTTransformationCustomizer( CompileStatic,(1) extensions:['robotextension.groovy'])(2))def shell = new GroovyShell(config)def robot = new Robot()shell.setVariable('robot', robot)shell.evaluate(script)| 1 | Apply@CompileStatic transparently |
| 2 | Activate the type checking extension |
The script will run fine because the static compiler is told about the type of therobot variable, so it is capableof making a direct call tomove. But before that, how did the compiler know how to get therobot variable? In factby default, in a type checking extension, settinghandled=true on an unresolved variable will automatically triggera dynamic resolution, so in this case you don’t have anything special to make the compiler use a mixed mode. However,let’s slightly update our example, starting from the robot script:
move 100Here you can notice that there is no reference torobot anymore. Our extension will not help then because we will notbe able to instruct the compiler thatmove is done on aRobot instance. This example of code can be executed in atotally dynamic way thanks to the help of agroovy.util.DelegatingScript:
def config = new CompilerConfiguration()config.scriptBaseClass = 'groovy.util.DelegatingScript'(1)def shell = new GroovyShell(config)def runner = shell.parse(script)(2)runner.setDelegate(new Robot())(3)runner.run()(4)| 1 | we configure the compiler to use aDelegatingScript as the base class |
| 2 | the script source needs to be parsed and will return an instance ofDelegatingScript |
| 3 | we can then callsetDelegate to use aRobot as the delegate of the script |
| 4 | then execute the script.move will be directly executed on the delegate |
If we want this to pass with@CompileStatic, we have to use a type checking extension, so let’s update our configuration:
config.addCompilationCustomizers( new ASTTransformationCustomizer( CompileStatic,(1) extensions:['robotextension2.groovy'])(2))| 1 | apply@CompileStatic transparently |
| 2 | use an alternate type checking extension meant to recognize the call tomove |
Then in the previous section we have learnt how to deal with unrecognized method calls, so we are able to write thisextension:
methodNotFound { receiver, name, argList, argTypes, call -> if (isMethodCallExpression(call)(1) && call.implicitThis(2) && 'move'==name(3) && argTypes.length==1(4) && argTypes[0] == classNodeFor(int)(5) ) { handled = true(6) newMethod('move', classNodeFor(Robot))(7) }}| 1 | if the call is a method call (not a static method call) |
| 2 | that this call is made on "implicit this" (no explicitthis.) |
| 3 | that the method being called ismove |
| 4 | and that the call is done with a single argument |
| 5 | and that argument is of typeint |
| 6 | then tell the type checker that the call is valid |
| 7 | and that the return type of the call isRobot |
If you try to execute this code, then you could be surprised that it actually fails at runtime:
java.lang.NoSuchMethodError: java.lang.Object.move()Ltyping/Robot;
The reason is very simple: while the type checking extension is sufficient for@TypeChecked, which does not involvestatic compilation, it is not enough for@CompileStatic which requires additional information. In this case, you toldthe compiler that the method existed, but you didn’t explain to itwhat method it is in reality, and what is thereceiver of the message (the delegate).
Fixing this is very easy and just implies replacing thenewMethod call with something else:
methodNotFound { receiver, name, argList, argTypes, call -> if (isMethodCallExpression(call) && call.implicitThis && 'move'==name && argTypes.length==1 && argTypes[0] == classNodeFor(int) ) { makeDynamic(call, classNodeFor(Robot))(1) }}| 1 | tell the compiler that the call should be made dynamic |
ThemakeDynamic call does 3 things:
it returns a virtual method just likenewMethod
automatically sets thehandled flag totrue for you
but also marks thecall to be done dynamically
So when it comes time for the compiler to generate bytecode for the call tomove, since it is now marked as a dynamic call,it falls back to the dynamic compiler and lets it handle the call. And since the extension tells us that the returntype of the dynamic call is aRobot, subsequent calls will be done statically!
Some might wonder why the static compiler doesn’t do this by default without an extension. It is a design decision.If the code is statically compiled, we normally want compile-time type safety and the best performance.If unrecognized variables/method calls were automatically made dynamic, you’d partially lose compile-time type safety.Typos and some scenarios of incorrect typing would no longer be caught at compile time!
In short, if you want to have mixed mode compilation, ithas to be explicit, through a type checking extension, sothat the compiler, and the designer of the DSL, are totally aware of exactly when typing rules are relaxed.
makeDynamic can be used on 3 kinds of AST nodes:
a method node (MethodNode)
a variable (VariableExpression)
a property expression (PropertyExpression)
If that is not enough, then it means that static compilation cannot be done directly and that you have to rely on ASTtransformations.
Type checking extensions look very attractive from an AST transformation design point of view: extensions have accessto context like inferred types, which is often nice to have. And an extension has a direct access to the abstractsyntax tree. Since you have access to the AST, there is nothing in theory that preventsyou from modifying the AST. However, we do not recommend you to do so, unless you are an advanced AST transformationdesigner and well aware of the compiler internals:
First of all, you would explicitly break the contract of type checking, which is to annotate,and only annotate the AST. Type checking shouldnot modify the AST tree because you wouldn’t be able toguarantee anymore that code without the@TypeChecked annotationbehaves the same without the annotation.
If your extension is meant to work with@CompileStatic, then youcan modify the AST becausethis is indeed what@CompileStatic will eventually do. Static compilation doesn’t guarantee the same semantics atdynamic Groovy so there is effectively a difference between code compiled with@CompileStatic and code compiledwith@TypeChecked. It’s up to you to choose whatever strategy you want to update the AST, but probablyusing an AST transformation that runs before type checking is easier.
if you cannot rely on a transformation that kicks in before the type checker, then you must bevery careful
| The type checking phase is the last phase running in the compiler before bytecode generation. All other ASTtransformations run before that and the compiler does a very good job at "fixing" incorrect AST generated before thetype checking phase. As soon as you perform a transformation during type checking, for example directly in a typechecking extension, then you have to do all this work of generating a 100% compiler compliant abstract syntax tree byyourself, which can easily become complex. That’s why we do not recommend to go that way if you are beginning withtype checking extensions and AST transformations. |
Examples of real life type checking extensions are easy to find. You can download the source code for Groovy andtake a look at theTypeCheckingExtensionsTestclass which is linked tovarious extension scripts.
An example of a complex type checking extension can be found in theMarkup Template Enginesource code: this template engine relies on a type checking extension and AST transformations to transform templates intofully statically compiled code. Sources for this can be foundhere.