Hatching New Methods in Mid-air #
Ken Miller sent in a very interesting class, which creates new methods as they are needed. And, also, the respond_to?
method is hacked to respond positively in these cases. I could see this kind of thing going into Rails so you can Student.find_classes
or Student.find_grades
. That sort of thing.
class MethodMagic alias_method :old_respond_to?, :respond_to? def respond_to? (method, include_private=false) old_respond_to?(method.to_sym, include_private) || create_dynamic_method(method) end alias_method :old_method_missing, :method_missing def method_missing(method, *args) if create_dynamic_method(method) send(method.to_sym, *args) else old_method_missing(method) end end # If the method name conforms to our rigorous specifications, # create a new body on the fly and define the sucker def create_dynamic_method(method) if method.to_s.match /^find_by_/ self.class.send(:define_method, method.to_sym) { |*args| puts "Called #{method}(#{args.join ", "})" } true else false end end # just here as an example of a normal method def find; end end mm = MethodMagic.new p mm.respond_to?(:find) # => true p mm.respond_to?(:find_by_letters) # => true mm.find_by_letters("a", "b", "c") # prints 'Called find_by_letters(a, b, c)' mm.find_by_numbers(1, 2, 3) # prints 'Called find_by_numbers(1, 2, 3)' p mm.respond_to?(:poopy) # => false mm.poopy # raises NoMethodError
What about you? Do you have any metaprogramming tricks for us? If nothing else, read Ruby/X11 source and get back to me.
Jamis
Well, Rails already has Student.find_by_first_name and so forth, though it doesn’t build new methods as they are requested. Doesn’t OpenStruct do something like this, too? (I’ve never really looked, so I don’t know for sure.)
chris2
I played with runtime-defined methods too some time ago. If the only reason you do this are speed issues, please benchmark and profile before you try this method.
In my tests, using method_missing only was faster than defining the methods on the fly (esp. on subsequent calls), I think this is because of Ruby’s method caching.
MenTaLguY
Depending on the approach used to add the method, there will be varying degrees of overhead when calling it.
While ugly, eschewing define_method and directly eval-ing a string containing a
def
clause will result in a method involving the least overhead to call.Of course, with string evals you don’t get to take a closure or any of that other good stuff, and you have to be very careful about sanitizing/untainting your strings if you try to interpolate values into them—it’s quite easy to introduce a security hole!
If you do dynamically add methods, define_method with a block is the cleanest and safest approach by far.
Always profile before deciding to do things the ugly way. Remember Knuth: “Premature optimization is the root of all evil.”
Dave
I’ve read Redhanded for a while now, and sure, it’s funny, and sure I love Ruby…
but what the heck is the “good idea” that stands behind “creating methods at runtime” when it’s not possible for those methods to have guts that work correctly.
Unless I’m mistaken, “mm.perfectly_translate_english_to_arabic(‘hello, world’” won’t work at all.
I’m not trying to be mean, I just want to know “why this is a good idea”...
Lucas C.
Just as a technical note, Student.find_classes and Student.find_grades would be class methods, which wouldn’t make sense. Only student objects would have classes associated with them. s = Student.find 1; s.find_classes
Ken Miller
First of all, thanks for the feedback. That’s what I was hoping for when I posted it.
For certain kinds of things, it IS possible for them to have guts that work correctly, a la the find_* methods in ActiveRecord. Essentially, you’re parsing your method names and creating your own mini-language. (Pause while any java programmers reading run screaming from the room.) It means you don’t have to explicitly create every single permutatation of a class of methods if the meaning can be reasonably inferred from the method name.
As for performance, Mentalguy is correct, using class_eval (not instance_eval!) is about 20% faster, at least for the trivial cases I profiled. That surprised me. Both approaches are about twice as fast (in my tests) as just doing the work in method_missing every time, assuming you call the method lots of times. Given that chris2 seems to have seen different results, profiling in your own environment is a must if performance matters.
This approach might be cleaner and faster if the runtime were more aware of it…
why
I don’t think the idea here is to save memory or speed. The idea behind metaprogramming is to teach Ruby your conventions and let it do some guessing, in order to save you some code.
Think of how handy dynamic methods could potentially be in an extension like FXRuby, which has hundreds of wrapper methods, many of which could be minimized if, at some point, it could all be boiled down to a bit of introspection against the extension itself. You know?
Nothing against FXRuby. I only mention it because I’ve been working with it and its enormity of late.
Dave: This isn’t a “best practices” blog. I’m not about teaching a right or correct way here. In fact, I’d rather unearth some questionable practices, in the possibility that they may just need to be given a second chance. And I like the harshness: who can blame your gag reflex?
chris2
I just don’t see the point of dispatching the name and defining the methods when you could just dispatch… YAGNI .
MenTaLguY
What _why said. The point is to let you do things the most expressive way (the inverse of what I mean by “ugly”).
Fast or small, you can deal with later if you have to. Usually you don’t.
why
chris2: Well, one difference is that
MethodMagic#methods
will include any methods you’ve called.Dave
why: Oh, I get it now. Actually, I kind of did think this was a “best practices + odd things” blog. d’oh. :)
Comments are closed for this entry.