Ideas for Improving Ruby

Posted by Rick DeNatale Mon, 23 Apr 2007 17:30:00 GMT

On Ruby is having another blogging contest this month, encouraging Ruby bloggers to discourse on what changes could be made to the Ruby language without making it something which isn’t Ruby.

Have a look at the On Ruby announcement to see what others have to say about this. Here’s what I’ve got to say, for what it’s worth.

First, I’ll give my personal opinions on some of what’s being proposed by some of the posts stirred up by this contest, then I’ll give my “modest proposal.”

What I wouldn’t change

Way back in the early days of my IBM career, I shared an office with a guy who was one of the ‘systems programmers’ who maintained and enhanced the mainframes on which we did our development. My office-mate was a nice guy, and he liked to add nice little features to the system. Unfortunately, sometimes these features weren’t all that well conceived, and did more harm than good. I took to calling these his ‘decremental’ improvements.

Here are some changes to Ruby which others have proposed, which I feel don’t fit into the idea of changing Ruby without making it Ruby anymore:

I wouldn’t muck around with adding type annotations to method prototypes.

This position shouldn’t surprise anyone who is a regular reader of this blog. For example, one of my fellow bloggers proposes allowing something like:

   def method(Integer i, String s)
      ...
   end

The argument seems to be that this DRYs up code like:

   def method(i, s)
      raise HolyHell unless i.kind_of?(Integer) && s.kind_of?(String)
      ...
   end

But in a dynamic language such as Ruby, such overly defensive coding really shouldn’t be encouraged. Don’t kill your ducks before theyv’e been conceived!

If you want to code like this, go ahead. No one can stop you. Do some metaprogramming, if you really have to so that you can write, say:

   def_method_with_typed_parameters :method, [Integer, 'i'], [String, 's'] do
      ...
   end

But there’s no need to muck up the core of a perfectly good dynamically typed language like Ruby with stuff like this.

I would Keep Ruby compilation one-pass.

Another idea was to allow forward references to methods, constants, variables and the like. Perl allows this and the value is that you can keep stuff which is editable by non-technical people at the beginning of a file, but this stuff can still reference things defined later in the file.

This requires a two-pass compilation, which first gathers up all the definitions then looks for an prucesses the uses of those definitions.

But it’s a strength of Ruby that it doesn’t work like this. In Ruby definitions are processed as the code is run. This allows powerful features like redefinition and wrapping of methods.

Besides, Ruby provides lots of ways to accomplish the same effect in even cleaner ways. Use the BEGIN{} and END{} features. Put the ‘editable by non-technical stuff’ in a separate file, and require or load it. Better yet, don’t necessarily think that those files have to include or be completely Ruby source. Some non-technical users would no doubt deal better with formats like YAML, or erb. This seems to work quite well to separate the concerns of programmers and designers in Rails for example.

Would I Fix the Ambiguity Between Methods and Local Variables?

I might want to somehow address the confusion which arises over the parsing of ‘barewords’ in Ruby.
People get tripped up when code like this doesn’t work as they expect:

class Point
   attr_accessor :x, :y
   
   def initialize(init_x,init_y)
      x, y = init_x, init_y
   end
end

p Point.new(10,10).x => nil

The problem here is that when the Ruby compiler sees variables like x, and y it doesn’t know whether they are local variables, or methods, and it treats them as local variables, which means that the above code needs to be written as:

class Point
   attr_accessor :x, :y
   
   def initialize(init_x,init_y)
      self.x, self.y = init_x, init_y
   end
end

p Point.new(10,10).x => 10

But it seems that the only way to fix this would be to mark local variables with yet another sigil. Sigils are the characters used to mark globals like the $ in $FILENAME, or instance variables like @x, etc.

And it seems to me that this is too high a price to pay.

Okay, So What Might I Change in Ruby?

So having rejected several ideas about how to improve Ruby, what do I think might improve the language?

As a long-time Smalltalker, I’d love to see Ruby acquire a more dynamic development environment inspired by the 27 year old Smalltalk IDE. But to enable this, I think that Ruby would benefit from a few changes.

Make the Runtime First Class!

Smalltalk implementations expose their workings to Smalltalk applications in the form of objects. Although Ruby provides a certain amount of access to threads and processes, this access is limited. Kernel#caller returns an array of textual descriptions of the methods on the stack frame rather than actual stack frame objects which can be used to obtain information about the run-time state.

While add-ons like ruby-debug have worked around these limitations with extensions, it would be nice if this were part of the core language.

Provide a standard Ruby compiler.

One of the things which makes building tools for Ruby difficult is the lack of a standard way to parse Ruby code. Extensions like parsetree allow access to the internal abstract syntax tree which the ruby parser generates, but the AST itself is not well documented, and is subject to change. It would be nice if this could be standardized as part of the language.

Free the Metaclass!

One of the ways in which Ruby differs the most from Smalltalk, is how it hides aspects of the relationships between class objects. In Smalltalk, every object has a class, classes are objects themselves, so classes have classes which are called metaclasses. Actually they’re not just called metaclasses, they are instances of a class called Metaclass, just as classes are instances of the class Class. The metaclass of a class named “C” is named “C Class”. In Smalltalk there is a metaclass inheritance hierarchy which parallels the class inheritance hierarchy, the superclass of the metaclass is the metaclass of the superclass. In other words, for a class C with superclass D, the superclass of “Class C” is the metaclass of D, known as “Class D.” Smalltalk Class and Metaclass both are subclasses of a class called Behavior which provides the ability to have a collection of methods among other things.

The behavior of a Smalltalk object comes from the chaining of these behaviors through the superclass link. For a given object this chain is anchored by the objects class field. Method dispatching is accomplished by traversing this chain until either a method with the desired method selector is found, or if it’s not, starting over and looking for a method which handles unimplemented selectors.

The above should provoke a certain sense of deja vu in Ruby programmers. In Ruby, something like this actually happens under the covers, but for some reason, it’s carefully hidden from the Ruby programmer. Instead of visible metaclasses, Ruby uses singleton classes of classes. The same mechanisms used to provide instance specific behavior (i.e. instance methods) are used to provide class behavior.

As in Smalltalk, the Ruby implementation chains ‘behavior’ objects using a ‘superclass’ chain. An object’s klass (SIC) field points to either it’s class or singleton class if it has one, in which case the singleton class’s superclass field points to the original class. The superclass field in each class points to the superclass. Method lookup works just like in Smalltalk. But in Ruby, the superclass field is ‘overloaded.’ It’s also used to insert methods defined by modules into the behavior chain. This is accomplished by chaining in a specially marked “Included Class” which contains a reference to the Module’s method table.

Note that the name of the field in Ruby objects which anchors the behavior chain is called ‘klass’ rather than ‘class’. One reason for this is that the Object#class method, doesn’t just return the object referenced by this field. It skips over singleton classes, and the special “Included Classes.”

Since in Ruby, a metaclass is implemented as a singleton class, if you ask a class for it’s class, you will always get back the answer Class. All classes look like instances of class. It’s not that classes have classes of their own, just that Ruby hides these.

Personally, I think that the case for hiding metaclasses from the Ruby programmer is weaker than the case for hiding singleton classes which are for the purpose of providing instance behavior, or for hiding “included classes.” One interesting side effect of this hiding is that protected class methods don’t make sense, since all classes are logically instances of the same class. If protection were based on the true ‘metaclass’ then protected class methods would make sense.

But the real thought motivating this suggestion is that if metaclasses were made visible in Ruby, certain aspects of metaprogramming might be clearer, and it might spur additional ideas to support better IDE like functions.


Comments

  1. malcontent about 11 hours later:

    I can’t believe you left out named parameters. I can’t stand passing in hashes.

  2. Samuel. P about 12 hours later:

    Maybe a mention of overall speed would help as well?

    BTW, isn’t Ruby interpreted? I am confused as to what you mean by a ‘standard Compiler’. Did you mean to propose a compiler for it for generating compiled code or just a more standardized interpreter?

  3. Devin Ben-Hur 1 day later:

    @Samuel.P: A compiler translates from the text of a source language to a target language. The target need not be machine code. In cRuby’s case the target is an internal Abstract Syntax Tree (AST) which is interpreted by the runtime.

  4. Damian 1 day later:
    
      def method(Integer i, String s)
    
    

    “The argument seems to be that this DRYs up code”

    Permit me to disagree a little (but not for the reason you’d expect). One of the nice things about this is that I can have several methods with the same name, but handling the string case, the integer case, etc. My conditional cases are now distinct methods method(String) method(Integer) method(Date) etc.

    But that only covers the type. Why not a more general pattern match? See Haskell and (iirc) Scala.

    
      def method(Integer === i) ...
      def method(i == 0) ...
      def method(i > 0) ...
    
    

    Well the syntax is hokey, but it’s a great feature in other languages.

  5. magic8ball@gmail.com 2 days later:

    Definately has to be the compiler, what would be good would be having a profiler and a compiler. The profile would suggest things in the code that could be modified to make for a faster compiled version

    Maybe the solution would be to compile down to really efficient Java Byte code, XRuby not JRuby that could be deployed on a App Server

  6. Rick DeNatale 7 days later:

    Damian,

    It may be a matter of taste, but I just don’t think that this kind of method/operator overloading really fits into to Ruby.

    Interestingly enough, as I understand it, some of the earliest versions of Smalltalk used a form of pattern matching as part of method dispatch. Classes actually parsed method invocations at run-time to figure out what to do.

    It didn’t stand the test of time though, for one thing it made it almost impossible to read code, since you had to know too much about the implementation of the objects you were invoking to get a clue as to what the message meant, which kind of defeated the purpose of method encapsulation.

  7. Rick DeNatale 7 days later:

    Samuel, and Devin,

    And the whole notion of interpreted vs. compiled languages is a bit squirrelly.

    Languages aren’t interpreted per-se, and there’s really a continuum of how high-level the representation produced by the compiler.

    With many computer architectures, even machine code is interpreted at some level, what we think of as machine-level object code is often being ‘interpreted’ by micro-code inside the processor.