Hi,
I've been using method_missing overly much in my code lately, and it's
prompted me to think a lot about it's limitations. I've been wishing
for a version of method_missing that allows the dynamic methods to act
more like they are real methods on the object. I think this could be
done by implementing a new method hook especially for dynamic methods:
dynamic_method.
dynamic_method would be called before method_missing if a method
lookup fails. If dynamic_method fails to handle the message,
method_missing will recieve the message for handling.
A couple of the immediate benefits afforded by a good implementation
of a dynamic method handler:
- the object will respond_to? the method
- you can call method(:foo) to get a copy of the dynamic method.
dynamic_method would be used something like this:
class Foo
def dynamic_method(name)
if name.to_s =~ /^foo/
# create the method (as a proc)
return lambda do |*args|
args.map{|arg| name.to_s.sub(/^foo/, arg.to_s) }
end
end
end
end
f = Foo.new
==>#<Foo:0x589a58>
f.respond_to? :foobar
==>true
f.respond_to? :barfoo
==>false
f.foobar(*%w[one two three])
==>["onebar", "twobar", "threebar"]
f.barfoo(*%w[one two three])
NoMethodError: undefined method `barfoo' for #<Foo:0x589a58>
f.method(:foobaz).call(*%w[one two three])
==>["onebaz", "twobaz", "threebaz"]
As you can see, a user-defined dynamic_method returns either a
callable object (Proc, Method, etc) or nil. If dynamic_method(message)
returns nil, it is assumed that the object does not
respond_to?(message), and method(message) should raise a
NoMethodError.
And here's a lightweight example implementation:
module Kernel
alias_method :old_method_missing, :method_missing
def method_missing(name, *args, &block)
m = dynamic_method(name)
if m
m.call(*args, &block)
else
m = Kernel.instance_method(:old_method_missing)
m.bind(self).call(name, *args, &block)
end
end
alias_method :old_respond_to?, :respond_to?
def respond_to?(msg)
(old_respond_to?(msg) || dynamic_method(msg)) ? true : false
end
alias_method :old_method, :method
def method(name)
old_method(name)
rescue NameError => e
m = dynamic_method(name)
raise e unless m
m
end
def dynamic_method(name)
nil
end
end
Here's a lightweight version of OpenStruct written using dynamic_method:
class OpenStruct
def initialize(hash = {})
@table = {}
hash.each do |key, value|
@table[key.to_sym] = value
end
end
def dynamic_method(name)
if name.to_s =~ /\=\z/
lambda{|val| @table[name.to_s.chop.intern] = val }
elsif @table.keys.include?(name)
lambda{ @table[name] }
end
end
end
And using it:
os = OpenStruct.new
==>#<OpenStruct:0x511454 @table={}>
os.red = 23
==>23
os.blue = 42
==>42
os.green = 56
==>56
os.respond_to? :blue
==>true
os.respond_to? :periwinkle
==>false
os_blue = os.method(:blue)
==>#<Proc:0x0038743c@(eval):13>
os.blue = 1024
==>1024
os_blue.call
==>1024
This is not a complete idea (let alone implementation) at this time...
I just wanted to see if anyone had an opinion on whether the idea was
worth anything, or could make suggestions to improve the interface.
Now here's me second-guessing myself: The implementation is pretty
complicated; adding another dynamic message handler may not be worth
the confusion. It would be one more thing to explain to people, and
while method_missing is an elegant addition to a language, I'm not
sure this would be. Especially considering the need to add more
complexity to method lookups.
Still, I think that even if this idea here isn't worthy, putting it
out there might help someone else come up with a more elegant
solution.
So, any thoughts?
cheers,
Mark