Monday, May 30, 2011

Visible Script Variables: Using Groovy 1.8's @Field AST

My most common use of Groovy is for writing scripts. As such, I enjoy its characteristics that enable an improved script development experience, but I also notice features of Groovy that are less than desirable for writing scripts. One of the minor annoyances of Groovy script writing has been the inability to have defined script variables visible to methods defined in the script. The current version of the Groovy User Guide define these rules in the Scoping and the Semantics of "def" section. This section explains the problem:
When you define a variable in a script it is always local. But methods are not part of that scope. So defining a method using different variables as if they were attributes and then defining these variables normally in the script leads to problems.

The problem is that locally declared (in the script) variables are not visible to methods defined in that same script. The Groovy 1.8 Release notes describes the problem in a slightly different way: "When defining variables in a script, those variables are actually local to the script's run method, so they are not accessible from other methods of the script."

Before Groovy 1.8, the standard tactic we Groovy script developers used to deal with this situation was to not define the local variables with the def keyword or with a static type declaration. In other words, the approach has always been to declare script variables with no def or static typing. This effectively places the undefined script local variable into "the binding," which the script's methods do have access to. The Scoping and the Semantics of "def" section of the Groovy User Guide puts it this way (emphasis present in source):
When is something in the Binding and when not? That's easy. When it is not defined, it is in the binding. The trick is - and that is admittedly not easy for Java programers - to not to define the variable before using it, and it will go into the binding. Any defined variable is local. Please note: the binding exists only for scripts.

The following Groovy code listing demonstrates this issue. The script defines three variables at the script level, one without 'def' or explicit type (will be in binding), one with 'def', and one with explicit type. All three local variables are used in respective methods without passing the argument to the method using it. Only the variable in the binding will work properly and the other two lead to MissingPropertyExceptions.

demoScriptVarDefinitionsWithoutFieldAst.groovy
#!/usr/bin/env groovy
// demoScriptVarDefinitionsWithoutFieldAst.groovy

/**
 * This variable is a binding variable because it lacks 'def' or type. It is
 * therefore visible to methods of this script.
 */
SPEED_OF_LIGHT_M_PER_S = 299792458     // visible to script methods

/**
 * This variable is a local variable because it is defined with 'def'. It would
 * have similarly been considered a local variable had it been explicitly
 * and statically typed to Double or BigDecimal. As a local variable, it is NOT
 * visible to methods defined on this script. 
 */
def SPEED_OF_SOUND_M_PER_S = 340.29    // NOT visible to script methods

/**
 * This variable is a local variable because it is explicitly typed. As a local
 * variable, it is NOT visible to methods defined on this script.
 */
Integer AVG_RADIUS_OF_EARTH_KM = 6371  // NOT visible to script methods 

useSpeedOfLight()
useSpeedOfSound()
useRadiusOfEarth()


/** Print the speed of light. */
def useSpeedOfLight()
{
   println "The speed of light is ${SPEED_OF_LIGHT_M_PER_S} m/s."
}

/** Print the speed of sound. */
def useSpeedOfSound()
{
   println "The speed of sound is ${SPEED_OF_SOUND_M_PER_S} m/s."
}

/** Print the average radius of the Earth. */
def useRadiusOfEarth()
{
   println "The average radius of the earth is ${AVG_RADIUS_OF_EARTH_KM} km."
}

When the code above is run via Groovy as-is, the local variable for speed of sound (which is local because it was defined with 'def') will be the first to break the script's execution. This is shown in the next screen snapshot.


When I comment out the single line that calls the useSpeedOfSound() method, the exception just shown is not encountered, but a similar one for another local script variable (which is local because it was defined with a static type) will be thrown. That is shown in the next screen snapshot.


If I comment out the call to the useRadiusOfEarth() method, the script runs fine, but I don't see either of the two constants printed out. This is shown in the next screen snapshot.


With the above in mind, the obvious solution and commonly used approach up to now has been to simply remove the def keyword and to remove the explicit static type definition so that all local script variables are in the binding. Fortunately, Groovy 1.8 provides a better alternative. The Groovy 1.8 release notes state:
Fortunately, the @Field transformation provides a better alternative: by annotating your variables in your script with this annotation, the annotated variable will become a private field of the script class.

The next code listing shows the previous script adapted to use this new Groovy 1.8 @Field annotation and the AST behind it.

demoScriptVarDefinitionsWithFieldAst.groovy
#!/usr/bin/env groovy
// demoScriptVarDefinitionsWithFieldAst.groovy

import groovy.transform.Field

/**
 * @Field makes this visible to script's methods.
 */
@Field SPEED_OF_LIGHT_M_PER_S = 299792458     // visible to script's methods

/**
 * @Field makes this visible to script's methods.
 */
@Field def SPEED_OF_SOUND_M_PER_S = 340.29    // visible to script's methods

/**
 * @Field makes this visible to script's methods.
 */
@Field Integer AVG_RADIUS_OF_EARTH_KM = 6371  // visible to script's methods

useSpeedOfLight()
useSpeedOfSound()
useRadiusOfEarth()


/** Print the speed of light. */
def useSpeedOfLight()
{
   println "The speed of light is ${SPEED_OF_LIGHT_M_PER_S} m/s."
}

/** Print the speed of sound. */
def useSpeedOfSound()
{
   println "The speed of sound is ${SPEED_OF_SOUND_M_PER_S} m/s."
}

/** Print the average radius of the Earth. */
def useRadiusOfEarth()
{
   println "The average radius of the earth is ${AVG_RADIUS_OF_EARTH_KM} km."
}

This works beautifully as shown in the next screen snapshot.



Conclusion

The @Field annotation and associated AST don't offer anything for non-script Groovy software, but it is a much appreciated addition for the writing of scripts in Groovy 1.8.

3 comments:

Sia said...

thanks, useful

Unknown said...

If I alter the value of a Field in a closure, does it take effect? From my test it seems I can alter the value in the closure but once i leave the closure the alterations is revoked. Somewhat confusing...Is this covered in the docs?

@DustinMarx said...

Mike,

The best documentation on closures and their effect on variable scope that I'm aware of is the Closures - Formal Definition page and the section "References to External Variables."

Dustin