This is taw's blog - the best cat and Ruby blog in the world. Now also about video games, and mining interesting data.

Wednesday, June 06, 2007

Ruby and methods with weird names

fire hose connection #2352 by Nemo's great uncle from flickr (CC-NC-SA)
RLisp identifiers don't have to be Ruby identifiers - field-get and .. are perfectly valid in RLisp but not in Ruby. Normally that's not a problem, Ruby Symbols can contain arbitrary strings, names of RLisp variables and functions are used only inside the compiler, and define_method(:method, ...) and send(:method, ...) couldn't care less about what :method looks like stringified.

There's just one tiny problem - using them breaks Delegator and other classes which use text-based runtime code generation.

class Delegator
def initialize(obj)
preserved = ::Kernel.public_instance_methods(false)
preserved -= ["to_s","to_a","inspect","==","=~","==="]
for t in self.class.ancestors
preserved |= t.public_instance_methods(false)
preserved |= t.private_instance_methods(false)
preserved |= t.protected_instance_methods(false)
break if t == Delegator
end
preserved << "singleton_method_added"
for method in obj.methods
next if preserved.include? method
begin
eval <<-EOS
def self.#{method}(*args, &block)
begin
__getobj__.__send__(:#{method}, *args, &block)
rescue Exception
$@.delete_if{|s| /:in `__getobj__'$/ =~ s} #`
$@.delete_if{|s| /^\\(eval\\):/ =~ s}
::Kernel::raise
end
end

EOS
rescue SyntaxError
raise NameError, "invalid identifier %s" % method, caller(4)
end
end
end
end

Delegator iterates over all methods of an object (obj.methods), and for each of them generates and compiles a simple wrapper:
def self.#{method}(*args, &block)
begin
__getobj__.__send__(:#{method}, *args, &block)
rescue Exception
$@.delete_if{|s| /:in `__getobj__'$/ =~ s} #`
$@.delete_if{|s| /^\\(eval\\):/ =~ s}
::Kernel::raise
end
end

If Ruby had real macros like RLisp it wouldn't be a problem. Unfortunately Ruby code is generated by simple string substitution, and if method is anything else than a Ruby identifier it breaks.

In Ruby def is not accessible in any way other than through eval. We cannot use define_method(method, ...) as in Ruby 1.8 it cannot define methods which take blocks. And it has different argument-handling semantics than def, so it wouldn't work anyway.
Every use of text-based runtime code generation is a failure of language's reflection model.
It's weird how Ruby can be so well-knows for its metaprogrammability, and at the same time have most of it inaccessible through any means other than text-based eval. I think improving metaprogrammability is a much more important for future viability of Ruby than improving performance or other popular whining points.

So what can RLisp do:
  • Accept that Delegator and everything that uses it like tmpfile and webrick is broken.
  • Mangle all non-Ruby symbols into valid Ruby symbols. So (method foo-bar ...) would really define foo_bar etc.
  • Monkey-patch Delegator to ignore such methods instead of raising an exception. Ugly.
  • Monkey-patch Delegator to delegate such methods with slightly incorrect define_method instead of raising an exception.
  • Improve Ruby metaprogrammability.

2 comments:

Piers Cawley said...

Yes, yes, yes, 1000 times yes.

Ruby is so nearly there for some stuff, and in other places it's pure pain. I don't mind the lack of macros so much as I mind the lack of metaquoting.

Mikoangelo said...

I agree, it's so absolutely horrible you have to do that, and it has bit me oh so many times. At least it's fixed in 1.9.

We reap what we sow.