Those Groovy adepts who want to skip to the punchline can check out my best-seller JIRA ticket: “ExpandoMetaClass sometimes, but sometimes MetaClassImpl“.
For the rest of us, check out some odd behavior in Groovy.
class A {} def a = new A() a.metaClass.greet << { println "Hello, World!" } a.greet()
Looks great. Looks like a classic “Metaprogramming in Groovy” example, right down to including “Hello, World!”. So, I’m off to run it…
Exception thrown: groovy.lang.MissingPropertyException: No such property: greet for class: groovy.lang.MetaClassImpl groovy.lang.MissingPropertyException: No such property: greet for class: groovy.lang.MetaClassImpl at Script0.run(Script0:4)
WTF? Weird…where’s that “MetaClassImpl” come from? Where’d my ExpandoMetaClass go?
So, right after executing the first line, and I check out the metaClass attached to the class.
class A {} println A.metaClass.class.simpleName // prints "ExpandoMetaClass"
Okay, good, so I’ve got it. The awesome part is what comes next.
def a = new A() a.metaClass.greet << { println "Hello, World!" } a.greet() // prints "Hello, World!"
Hey, whaddyaknow? Now it works out great.
Turns out that you only get the ExpandoMetaClass if your class statically calls “.metaClass” before the variable is instantiated. Either that, or you need to call ExpandoMetaClass.enableGlobally().
14 Comments
So… the bug is you’re adding methods to a class at run time?
:-)
//
class A {}
A.metaClass.greet << { println “Hello, World!” }
def a = new A()
a.greet()
//
will work.
“By default methods are only allowed to be added before initialize() is called. In other words you create a new ExpandoMetaClass, add some methods and then call initialize(). If you attempt to add new methods after initialize() has been called an error will be thrown.”
This is taken right from the API.
I wouldn’t call it a bug, it’s supposed to work like this :P
I read the JIRA,
My question to you would be, why do you want to create a property on an instance? What advantage, or functional need would present itself to warrant doing that, rather than on the actual class?
Kind regards,
@npiv
Are you asserting that it’s explicitly unsupported to add instance methods to Groovy? I find that hard to believe, particularly considering the conversation on the JIRA never brought it up.
Also @npiv
My question to you would be, why do you want to create a property on an instance? What advantage, or functional need would present itself to warrant doing that, rather than on the actual class?
Well, sidestepping Brian’s snarky assertion that *any* kind of runtime mangling is nonsensical, the argument is pretty simple. Basically, the class level itself is too broad a stroke sometimes. And Groovy’s quasi-typing actually makes things even trickier.
Say I wanted to track how many times methods were called to debug a tricky part of code, or add some stopwatch functionality for generating logs, or dynamically add actions to certain Grails controllers if another conventional file exists (e.g. externalized validation), etc., etc.
Or say that I have an expensive calculation which can be optimized based on certain properties which I know to be true about a property — for instance, adding a lazy thunk to a String which evaluates to the contents of the file it is assumed to name.
Or say I don’t know the class of an object (because I’m relying on duck typing), but if it moves through a certain segment to code, I want to tag it and then refer to it later.
“Say I wanted to track how many times methods were called to debug a tricky part of code,”
class A {
def doIt() {
println “nike says”
}
}
// dynamic method call counting
A.metaClass.callCounter = new HashMap()
A.metaClass.invokeMethod = { name , args ->
A.metaClass.getMetaMethod(name,args).invoke(delegate,args)
def counted = delegate.callCounter["${name}"]
delegate.callCounter["${name}"]=counted?counted+1:1
}
// end
def a = new A()
a.doIt()
a.doIt()
assert a.callCounter["doIt"] == 2
You define the dynamic method counter on the class, but you’re still tracking the individual counts per instance.
You make a good point on the end, about duck typing, but that could be tackled in it’s own right with the use of categories.
Perhaps we’re not on the same page, and I have misunderstood what you mean by defining methods on individual instances, but please indulge me. Debate is healthy for us developers ;)
Kind regards,
@npiv
You’re assuming in all those cases that I 1) have write access to the class definition, 2) know the class of the object being passed through, and 3) consider it acceptable for behavior that I want on a single instance to become global to the entire type.
@npiv
You also missed the point of this one:
Say I wanted to track how many times methods were called to debug a tricky part of code,
The point is that I want a piece of functionality to be on a single instance of a class for a limited period of time. So I would add the functionality before the object instance went into the tricky part of code, and pull it off afterwards.
@Robert
Thanks for getting back with the example. We weren’t entirely on the same page, as I feared. But I understand clearly now what behavior you want to achieve. And indeed why this is troublesome with groovy metaclass.
To get back on track with the title of this post, I guess the issue is with what I expect from metaclass being different from your expectations.
“The point is that I want a piece of functionality to be on a single instance of a class for a limited period of time” – Robert
To me, metaclass offers dynamic capabilities in an OO world.
The dynamic part means I can add/intercept/remove methods at runtime. But only at the class level, because I expect my instances to share the same behavior.
What you want to achieve at the instance level, in my view, is a different matter. And doesn’t comply with Object oriented design as I see it. You are speaking of a mutation. One instance that grows out of its class, and can do more than its siblings.
That being said, It is a very interesting requirement, and if ever implemented, does open the door for some sexy coding possibilities.
So to conclude on my end, this is what I expected from metaclass, and thus I consider it a feature.
and once again
Kind regards, ;)
@Robert
“Are you asserting that it’s explicitly unsupported to add instance methods to Groovy? ”
yes, the quote I posted earlier comes right out of the API
“By default methods are only allowed to be added before initialize() is called. In other words you create a new ExpandoMetaClass, add some methods and then call initialize(). If you attempt to add new methods after initialize() has been called an error will be thrown.”
http://groovy.codehaus.org/api/groovy/lang/ExpandoMetaClass.html
@npiv Odd that an error isn’t thrown in this case, then.
I’ll let you fire that JIRA ticket off.
It’s also odd that nobody mentioned the “instances can’t add new metaclass capabilities” throughout the JIRA ticket I had opened.
@npiv
There seems to be two general minds on this conversation (hence the “Bug or Feature” question).
One stance is that the entire concept of an “isntance metaClass” is nonsensical — that behavior is defined at the class level, so mangling the behavior of an instance mandates mangling the behavior at the instance level.
This is not Groovy’s approach.
Groovy allows mangling of instance metaClasses — indeed, you can assign a whole new metaClass to a particular instance, which will completely rewrite its type. And it also allows mangling existing metaClasses at an instance-by-instance level (see my previous example, and the JIRA ticket I opened). To me, this is the way Groovy should work, since the type system is already de facto out the window. Duck typing and API mangling makes the class’s type a suggestion — at best — as to a type’s behavior. The pseudo-object functionality offered by hashes even further blurs the lines around types. So, given that types aren’t terribly relevant in Groovy, I’d say that we should go ahead and let mutability happen wherever: we’ve already got enough rope to hang ourselves, why not actually get enough to do something useful? And this behavior is inline with the cool kid on the block (Ruby).
Now, whether this is the best approach or not is a conversation for a different post.
@Robert
“Odd that an error isn’t thrown in this case, then.”
Yes odd indeed, I was expecting that piece of code to fail.
I guess for performance reasons an object either has a metaclass or not attached to it. The default being not. the ExpandoMetaClass.enableGlobally() will probably just make sure that ExpandoMetaClass is enabled on all classes by default.
Which is what you are manually doing when calling X.metaClass, for the class X.
that being said, the code you posted is in contradiction with the API, so I might let myself be heard on that long JIRA ticket soon :P
something is not quite right, I might change my vote to bug soon
@npiv
If you do the “x.metaClass” without doing the “X.metaClass” beforehand, you get a metaClass — of type MetaClassImpl, which does not have the same functionality as an ExpandoMetaClass. But if you do X.metaClass first (before x is instantiated) then you’ll get an ExpandMetaClass when you do “x.metaClass”.
This is what I was asserting as a bug.
Which is what you are manually doing when calling X.metaClass, for the class X.
Really? By accessing the metaClass, I’m manually attaching an ExpandoMetaClass? I thought I was just checking the static metaClass field off of the X class.
This is bug #2 in this issue: accessors should not have side effects.
You can talk on the Jira bug, but you might have better luck hitting the Groovy dev mailing list and asking exactly what *is* the intended behavior for instance-level metaclasses.
3 Trackbacks
[...] Bug Or Feature, You Decide: Groovy MetaClass Programming [...]
[...] Bug Or Feature, You Decide: Groovy MetaClass Programming [...]
[...] Bug Or Feature, You Decide: Groovy MetaClass Programming [...]