Matt posted a really elegant piece of code today that generates Graphviz files (suitable for importing into OmniGraffle) of your Rails ActiveRecord relationships. It’s pretty neat, and certainly handy for getting to know foreign codebases.
There’s one neat trick in there, though, that I wanted to expand on, as Matt breifly chatted to me about the problem earlier today over IM - namely, how you get the actual class object so that you can call reflect_on_all_associations on it.
In Ruby, it’s easy to dynamically call methods - you can put the name of the method into a string, and then simply run Object.send(methodname). Getting the actual Class Object for a particular object - that’s much trickier.
There’s the obvious solution of using eval. So, to get all the methods on your classname:
classname = 'Integer'
eval classname + '.methods'
but that, of course, is pretty nasty and kludgy. This is Ruby, after all; there’s got to be a better solution, right?
There is. If you look in the Pickaxe, you’ll find that Class Names Are Constants:
All the built-in classes, along with the classes you define, have a corresponding global constant with the same name as the class.
So this means that by passing the class name to the const_get method on the Kernel module, we’ll confirm if a class exists with that name (eg Integer). Then, because that constant is really a reference to an object of the same name, by sending a message (the method calal) to the constant, it will be passed on to the object and run (which is the best way I’ve got of explaining this). Job done! To use the previous example:
classname = 'Integer'
Kernel.const_get(classname).methods
Which is, you must admit, a bit more elegant and maintainable than the evil that is eval.
Trackback: Generic actions for Rails subclasses
Trackback: Andrew’s Blog » Getting Reference to a Class with a String Class Name in Ruby
Simon Willison said... 1
How does that work with classes with the same name as other classes, but in different modules? I never really figured out classes and modules when I was playing with Ruby so this is probably a dumb question.
ctran said... 2
There’s a shortcut in Rails for this, ‘Integer’.constantize.methods will do the same.
bobo said... 3
Re: classes with same name? In a way you’ve stumbled on the very purpose of ruby modules, as I understand it: it’s a mechanism for disambiguating classes, for avoiding class name clashes. A module provides a http://en.wikipedia.org/wiki/Namespace
Anyway, to answer your question:
ModuleName.const_get(”MyClass”)
Tom said... 4
Simon and I have now discussed this over IM. As Bobo points out, we discussed namespacing, and how classes not inside a module end up as part of Kernel. (And, of course, that Kernel is a module that Class includes).
mexxx said... 5
Very nice. I have another question in a way related to this.
Let’s say that I have a class BigTree and I have a string ‘big_tree’ how do you get the string translated to ‘BigTree’ so that I can use this translated string the way you showed?
Tom said... 6
No problems, mexxx. Have a look at this little blog post for some tips.
Once you’ve done that, you can then use what I describe in this post.
Zargony said... 7
mexxx, in Rails there’s the camelcase method:
“big_tree”.camelcase #=> ‘BigTree’
Gert said... 8
About constantize as alternative, doesn’t that use eval, which was precisely what this post wanted to avoid?
http://dev.rubyonrails.org/ticket/2856
Daniel said... 9
How would you do this if you don’t know your module and class in advance. For example I want to get an instance of ‘MyModule::MyClass’ it works with eval but neighter with constantize nor with Kernel. Thanks, Daniel
Joey said... 10
In (late) response to Daniel’s question:
Object.const_get(’MyModule’).const_get(’MyClass’)
=> MyModule::MyClass
Nate Klaiber said... 11
I recently had the need to do the same thing as discussed here. Since I used it in several places I just extended String to have a ‘to_class’ method that simply said:
def.to_class
Kernel.const_get(self)
end
So I could use something like “Product”.to_class which would then allow me to use all methods on Product.
This could even be extended more by using ‘classify’ or the like before you return the constant.
Nate Klaiber said... 12
Oops.
def to_class
…
end
Mirage said... 13
A nice one-liner to handle the case with classes nested in an arbitrary number of modules is the following:
class_name = “MyModule::MyClass”
class_name.split(’::’).inject(Kernel) {|scope, const_name| scope.const_get(const_name)}
Which as Nate pointed out could be put in a String.to_class method, or Kernel.get_class.
Chris Hoeppner said... 14
Time to dig into #inject. It’s a method I have not yet understood.
xprdr said... 15
inject is not used for the purpose you are trying to accomplish here. plus there are at least two types of injects:
http://www.java2s.com/Code/Ruby/Range/Injectarange.htm
and
http://www.java2s.com/Code/Ruby/Hash/injectwithregularexpression.htm
Michael A said... 16
Well done Mirage. I’ve never used inject for any real work but it makes short work of this problem.
btower said... 17
What makes the non-eval version more maintainable?