The best kittens, technology, and video games blog in the world.

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.