Tuesday, May 31, 2011

Groovy 1.8's @Canonical Transformation: Great Functionality, Less Code

The final example in my blog post Groovy 1.8 Transformations: @ToString, @EqualsAndHashCode, and @TupleConstructor demonstrated using the @ToString, @EqualsAndHashCode, and @TupleConstructor annotation-signified transformations on a single Groovy class. Although using the three of these together does work as shown in that post, Groovy 1.8 provides an ever easier approach for specifying all three. In this post, I look at using the @Canonical annotation-based AST transformation to have these methods all generated implicitly.

To demonstrate @Canonical, I begin by reproducing the aforementioned example that specified the three annotations separately. The Groovy class used then was called TheWholePerson and is shown in the next code listing.

TheWholePerson.groovy
@groovy.transform.TupleConstructor
@groovy.transform.EqualsAndHashCode
@groovy.transform.ToString(includeNames = true, includeFields=true)
class TheWholePerson
{
   String lastName
   String firstName
}

The above can be simplified by replacing the three specific annotations with the @Canonical annotation. This is shown in the next code listing.

CanonicalPerson.groovy
@groovy.transform.Canonical
class CanonicalPerson
{
   String lastName
   String firstName
}

There's not much code in the CanonicalPerson.groovy code listing, but there is more there than meet's the eye. For convenience, I next reproduce the test driving code listing I used in my previous post with added functionality to demonstrate CanonicalPerson in action.

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

def person = new Person(lastName: 'Rubble', firstName: 'Barney')
def person2 = new Person(lastName: 'Rubble', firstName: 'Barney')
def personToString = new PersonToString(lastName: 'Rockford', firstName: 'Jim')
def personEqualsHashCode = new PersonEqualsHashCode(lastName: 'White', firstName: 'Barry')
def personEqualsHashCode2 = new PersonEqualsHashCode(lastName: 'White', firstName: 'Barry')

// Demonstrate value of @ToString
printHeader("@ToString Demonstrated")
println "Person with no special transformations: ${person}"
println "Person with @ToString transformation: ${personToString}"

// Demonstrate value of @EqualsAndHashCode
printHeader("@EqualsAndHashCode Demonstrated")
println "${person} ${person == person2 ? 'IS' : 'is NOT'} same as ${person2}."
println "${personEqualsHashCode} ${personEqualsHashCode == personEqualsHashCode2 ? 'IS' : 'is NOT'} same as ${personEqualsHashCode2}."

// Demonstrate value of @TupleConstructor
printHeader("@TupleConstructor Demonstrated")
def personTupleConstructor = new PersonTupleConstructor('Whyte', 'Willard')
println "Tuple Constructor #1: ${personTupleConstructor.firstName} ${personTupleConstructor.lastName}"
def personTupleConstructor2 = new PersonTupleConstructor('Prince')  // first name will be null
println "Tuple Constructor #2: ${personTupleConstructor2.firstName} ${personTupleConstructor2.lastName}"

// Combine all of it!
printHeader("Bringing It All Together")
def wholePerson1 = new TheWholePerson('Blofeld', 'Ernst')
def wholePerson2 = new TheWholePerson('Blofeld', 'Ernst')
println "${wholePerson1} ${wholePerson1 == wholePerson2 ? 'IS' : 'is NOT'} same as ${wholePerson2}."

// Simplify the combination!
printHeader("Simplified via Canonical")
def canonicalPerson1 = new CanonicalPerson('Goldfinger', 'Auric');
def canonicalPerson2 = new CanonicalPerson('Goldfinger', 'Auric');
println "${canonicalPerson1} ${canonicalPerson1 == canonicalPerson2 ? 'IS' : 'is NOT'} same as ${canonicalPerson2}."

/**
 * Print a header using provided String as header title.
 *
 * @param headerText Text to be included in header.
 */
def printHeader(String headerText)
{
   println "\n${'='.multiply(75)}"
   println "= ${headerText}"
   println "=".multiply(75)
}

The output from running the above script is shown next. One important observation to make from this output is that the @Canonical does indeed provide implicit toString() support as well as implicit equals support. A second observation is that the output does not return name/value pairs like the example specifying the three annotation separately did for the toString() representation.


The output from using @Canonical uses default settings for toString() output. To override these settings, the @ToString annotation should be applied in conjunction with the @Canonical annotation as shown in the next version of the Groovy model class shown previously, this time called CanonicalToStringPerson.groovy.

CanonicalToStringPerson.groovy
@groovy.transform.Canonical
@groovy.transform.ToString(includeNames = true, includeFields=true)
class CanonicalToStringPerson
{
   String lastName
   String firstName
}

In the above code listing, the same line with @groovy.transform.ToString(includeNames = true, includeFields=true) that led to the Groovy class TheWholePerson's toString() returning field name/value pairs is added to the class using the @Canonical annotation. This allows for customization of the @ToString representation. When I add some lines of code to the test driving Groovy script shown above with new output indicates that name/value pairs are listed for the fields in the toString() representation.


There is an obvious benefit to using @Canonical if the "vanilla" versions of the three transformations it represents (ToString, EqualsAndHashCode, and TupleConstructor) are sufficient. However, once it must be overridden with one or more individual annotations for customization, it might be preferable to simply specify the individual annotations.

There are several other references for additional reading and/or different perspectives on @Canonical. The Groovy 1.8 release notes reference John Prystash's blog post Groovy 1.8: Playing with the new @Canonical Transformation. Another useful reference is mrhaki's Groovy Goodness: Canonical Annotation to Create Mutable Class.

The @Immutable AST transformation has been available since Groovy 1.6 and is preferable to @Canonical when the state of the Groovy object should not change after instantiation. The advantage of @Canonical exists when the class state does need to be modified after its original instantiation, but the developer wishes to have much of the boilerplate code automatically generated.

Javap Proves What @Canonical Adds

Although my test code listed above shows the value of @Canonical, perhaps the best way to see what it adds to a normal Groovy class is to look at the javap output of a Groovy class without any annotations and to compare that to the javap output of a Groovy class employing the @Canonical annotation. For the "control" Groovy class that doesn't use any of these annotations, I again borrow from my previous post and that class (Person.groovy) is reproduced here.

Person.java
class Person
{
   String lastName
   String firstName
}

The javap output for the simple Person class looks like this:

javap Output for Person Class Class
Compiled from "Person.groovy"
public class Person extends java.lang.Object implements groovy.lang.GroovyObject {
  public static transient boolean __$stMC;
  public static long __timeStamp;
  public static long __timeStamp__239_neverHappen1306808397612;
  public Person();
  public java.lang.Object this$dist$invoke$1(java.lang.String, java.lang.Object);
  public void this$dist$set$1(java.lang.String, java.lang.Object);
  public java.lang.Object this$dist$get$1(java.lang.String);
  protected groovy.lang.MetaClass $getStaticMetaClass();
  public groovy.lang.MetaClass getMetaClass();
  public void setMetaClass(groovy.lang.MetaClass);
  public java.lang.Object invokeMethod(java.lang.String, java.lang.Object);
  public java.lang.Object getProperty(java.lang.String);
  public void setProperty(java.lang.String, java.lang.Object);
  public static void __$swapInit();
  static {};
  public java.lang.String getLastName();
  public void setLastName(java.lang.String);
  public java.lang.String getFirstName();
  public void setFirstName(java.lang.String);
  public void super$1$wait();
  public java.lang.String super$1$toString();
  public void super$1$wait(long);
  public void super$1$wait(long, int);
  public void super$1$notify();
  public void super$1$notifyAll();
  public java.lang.Class super$1$getClass();
  public java.lang.Object super$1$clone();
  public boolean super$1$equals(java.lang.Object);
  public int super$1$hashCode();
  public void super$1$finalize();
  static java.lang.Class class$(java.lang.String);
}

The Person class has "get" and "set" methods for its fields because Groovy provides these out-of-the-box for its property support. Although we see that it has hashCode() and equals(Object) implementations from its parent class, it does not have any of its own. Now, we can contrast this output against the javap output for the class with the @Canonical annotation.

javap Output for CanonicalToStringPerson
Compiled from "CanonicalToStringPerson.groovy"
public class CanonicalToStringPerson extends java.lang.Object implements groovy.lang.GroovyObject {
  public static transient boolean __$stMC;
  public static long __timeStamp;
  public static long __timeStamp__239_neverHappen1306808397590;
  public CanonicalToStringPerson(java.lang.String, java.lang.String);
  public CanonicalToStringPerson(java.lang.String);
  public CanonicalToStringPerson();
  public int hashCode();
  public boolean equals(java.lang.Object);
  public java.lang.String toString();
  public java.lang.Object this$dist$invoke$1(java.lang.String, java.lang.Object);
  public void this$dist$set$1(java.lang.String, java.lang.Object);
  public java.lang.Object this$dist$get$1(java.lang.String);
  protected groovy.lang.MetaClass $getStaticMetaClass();
  public groovy.lang.MetaClass getMetaClass();
  public void setMetaClass(groovy.lang.MetaClass);
  public java.lang.Object invokeMethod(java.lang.String, java.lang.Object);
  public java.lang.Object getProperty(java.lang.String);
  public void setProperty(java.lang.String, java.lang.Object);
  public static void __$swapInit();
  static {};
  public java.lang.String getLastName();
  public void setLastName(java.lang.String);
  public java.lang.String getFirstName();
  public void setFirstName(java.lang.String);
  public void super$1$wait();
  public java.lang.String super$1$toString();
  public void super$1$wait(long);
  public void super$1$wait(long, int);
  public void super$1$notify();
  public void super$1$notifyAll();
  public java.lang.Class super$1$getClass();
  public java.lang.Object super$1$clone();
  public boolean super$1$equals(java.lang.Object);
  public int super$1$hashCode();
  public void super$1$finalize();
  static java.lang.Class class$(java.lang.String);
}

In the above javap output, we can see that the expected parameterized constructor provided by @TupleConstructor is available as are the equals(Object), hashCode(), and toString() methods. The @Canonical annotation and its associated AST transformation did its job.


Conclusion

The introduction of @Canonical in Groovy 1.8 continues Groovy's theme of simplifying coding and providing for concise syntax with little unnecessary verbosity. Wikipedia's primary definition of "canonical" seems to fit the use of the newly available @Canonical: "reduced to the simplest and most significant form possible without loss of generality." The @Canonical annotation and underlying AST do indeed make the Groovy data class nearly as simple as possible while maintaining the canonical functionality associated with such data classes.

No comments: