In a recent article, I mentioned learning Groovy. After spending more time with it, I’m quite impressed by its elegant design.
While I haven’t delved into compiler/interpreter development, I’m fascinated by languages with unique features and paradigms like expression trees, lambdas, runtime code generation, Prototype-based programing, runtime weaving, and metaclasses.
Exploring the Groovy documentation (User Guide), I discovered many intriguing aspects. Groovy’s closures (“Closures”), similar to C# delegates, utilize compiler-generated classes inheriting from Closure/MulticastDelegate. Recognizing this, I realized that many features categorized as “Compile-time Metaprogramming - AST Transformations,” such as those resembling Boo’s syntactic macros, are essentially compiler tricks. While useful and elegant, they weren’t groundbreaking to me.
However, the “Dynamic Groovy - ExpandoMetaClass” section truly captivated me. Two key concepts stood out:
Expando: This concept, familiar from JavaScript, refers to objects expandable with new methods and properties. Expando objects form the foundation of prototype-based languages like JavaScript. Python objects share this expandability (both languages primarily use dictionaries for object properties and methods), and .NET offers a comparable ExpandoObject.
Metaclass: My initial encounter with metaclasses was during my Python days. While my Python experience has waned (despite its appealing features, the non-C syntax remains a hurdle for me), I recall metaclasses as a complex concept often reserved for experts. My understanding was that they enabled the creation of other classes, influencing their behavior during instantiation. This aligns with the Wikipedia definition:
“In object-oriented programming, a metaclass is a class whose instances are classes. Just as an ordinary class defines the behavior of certain objects, a metaclass defines the behavior of certain classes and their instances. Not all object-oriented programming languages support metaclasses. Among those that do, the extent to which metaclasses can override any given aspect of class behavior varies. Each language has its own metaobject protocol, a set of rules that govern how objects, classes, and metaclasses interact.”
It appears that metaclasses have gained traction for practical applications, with its usage in Django receiving significant praise.
In Groovy, metaclasses operate at both the class and instance levels, as both objects and classes possess a “metaClass” property. Note that Groovy treats classes as first-class objects, hence the “metaClass” property. “Person.metaClass” doesn’t access a static property but retrieves the class object (akin to “Person.getClass()”) and accesses its “metaClass” property. While standard Java classes lack this property, Groovy adds it. For clarification, refer to the Groovy JDK (not to be confused with Groovy API).
A primary application of Groovy metaclasses is the ExpandoMetaClass (EMC), allowing the addition of methods (closures) and properties to both class and instance objects. This modification dynamically extends all instances:
|
|
This straightforward approach mirrors the flexibility of JavaScript and Python. While these languages inherently treat objects as dictionaries, Groovy employs a more sophisticated mechanism. When compiled to JVM bytecode, each Groovy class becomes a Java class (along with additional classes for closures, categories, etc.). Java classes are inherently closed, prohibiting runtime expansion. Method invocation and property access are determined at compile time, with limited runtime dynamism through method overloading and vtables. So how does Groovy achieve this dynamic behavior?
The answer lies in Groovy’s method invocation process. A simple call like “myInstance.myMethod();” is cleverly transformed by the compiler into “ScriptByteCodeAdapter.invokeMethod,” triggering a chain of calls that involve metaclasses:
ScriptBytecodeAdapter.invokeMethod(…) (static method)
InvokerHelper.invokeMethod(…) (static method)
Invoker.invokeMethod(…) (instance method called on InvokerHelper’s single instance)
Invoker calls invokeMethod(…) on the MetaClass of our class (with exceptions, see below). It finds this MetaClass by looking it up in the MetaClassRegistry. The Invoker holds a single instance of this registry.
here illustrates this process. This introduces the “MetaClassRegistry” and its relationship to per-instance and per-class metaclasses. In essence, the “metaClass” property of the involved object plays a central role in finding the relevant metaclass.
Through research, including this nice pdf (note that it predates some Groovy updates), debugging, and code exploration, I’ve arrived at these conclusions:
- All user-defined Groovy classes implement the “GroovyObject” interface, granting them the “metaClass” property.
- Classes themselves also have a “metaClass” property.
- Initially, “Class.metaClass” (e.g., “Person.metaClass”) points to a “MetaClassImpl” instance.
- When an instance (“p1”) is created, its “metaClass” (“p1.metaClass”) references the same “MetaClassImpl” object as its class.
- Adding a method to either a class or instance metaclass replaces the “MetaClassImpl” with an “ExpandoMetaClass” object.
- If the class metaclass is modified, new instances reflect these changes, while existing instances retain the original behavior.
- Modifications to an instance metaclass are localized and don’t affect the class or other instances.
Refer to this code for a comprehensive explanation, including edge cases.
While the use of the “metaClass” property and “ScriptBytecodeAdapter” is clever, it might not surprise Python or JavaScript enthusiasts. The truly remarkable aspect is its applicability to standard Java classes:
|
|
This is where the “MetaClassRegistry” becomes crucial. Java classes, lacking the “metaClass” property, rely on this registry during method invocation. Essentially, it acts as a dictionary mapping class objects to metaclasses. Modern Groovy extends this to instances as well. The mechanism behind accessing the non-existent “metaClass” property in Java classes likely involves “methodMissing.”
Interception: This feature is what truly impressed me. Languages offer various ways to intercept method calls and property access for AOP-like tasks. Java and C# primarily rely on compile-time weaving or runtime dynamic proxies. Python offers decorators and metaclasses for compile-time decoration, while Python and JavaScript allow for runtime patching by manipulating method references.
Groovy excels in this area thanks to its “invokeMethod” mechanism. While implementing “GroovyInterceptable” for compile-time interception is interesting, the real power lies in dynamically adding “invokeMethod” to metaclasses at runtime. As explained earlier, method calls eventually reach the metaclass’s “invokeMethod,” enabling runtime interception. The inheritance behavior of metaclass additions (class vs. instance) applies here as well.
I’ve provided an example at a sample here.
A similar runtime interception approach in Python might involve “__getattribute__,” potentially added dynamically instead of within the class definition (as shown in this sample). Another surprising feature is Groovy’s support for Multiple Dispatch in type-hinted scenarios.
Categories: Another noteworthy feature is Groovy’s categories (Categories), similar to C#’s extension methods but more powerful. Their scope is limited to a block (“use…”), and they lack C#’s compile-time type limitations. This means a category defined for a “Child” class will function correctly even if a variable typed as “Parent” holds a “Child” instance at runtime, something the C# compiler wouldn’t permit.
MOP: Metaclasses are related to the concept of a Meta-Object Protocol (MetaObject Protocol):
“The metaobject protocol approach … is based on the idea that one can and should open languages up, allowing users to adjust the design and implementation to suit their particular needs. In other words, users are encouraged to participate in the language design process.”
While this definition sounds ambitious, Groovy’s MOP, though powerful, might not encompass all aspects (e.g., modifying inheritance or exception handling). It achieves its flexibility through hooks and extension points:
“Groovy’s MOP system includes some extension/hooks points that you can use to change how a class or an object behaves, mainly being: getProperty/setProperty: control property access invokeMethod: controls method invocation, you can use it to tweak parameters of existing methods or intercept not-yet existing ones methodMissing: the preferred way to intercept non-existing methods propertyMissing: also the preferred way to intercept non-existing properties” (source: here)
Interestingly, [a book about the DLR](http://my.safaribooksonline.com/book/programming/microsoft-dotnet/9781430230663/dynamic-objects/dynamicobject_class
) explores implementing a MOP in C# 4 using “DynamicMetaObject” (IDynamicMetaObjectProvider).
Groovy’s rapid evolution is commendable, with its designers constantly pushing boundaries (great plans for next versions). Two features particularly stand out:
- New Meta-Object Protocol
- Ability to pass expression trees/AST nodes as parameters (similar to C# 4’s expression trees)
Final Thoughts: Given my enthusiasm for Groovy, a .NET implementation (IronGroovy?) would be fantastic. However, the success of dynamic languages on .NET has been mixed. While Boo, IronPython, IronRuby, and others exist, they haven’t gained the same traction as their Java counterparts. This might stem from C#’s relatively greater expressiveness compared to Java. Even Jim Hugunin, a prominent figure in the Java, .NET, and Python worlds, acknowledged this (also thinks so):
“I will suffer some pain when I have to write code in Java now that I’ve learned to love the elegance of C#.”
Consequently, many .NET developers might not feel compelled to adopt a new language when C# adequately meets their needs, especially with its rapid evolution.
For those concerned about performance, I’d say (benchmarks aside): Who cares about performance when you can write such elegant code? :-D