Thursday, September 28, 2006

Why Add Syntax When You Have Ruby?

A question came up this morning on the JRuby Users mailing list about a feature Jython and Groovy support: constructors that initialize bean attributes. The example given used a JLabel:

JLabel(text, bounds = bounds, foreground = color)

Which ends up being roughly equivalent to:

x = JLabel(text)
x.bound = bounds
x.foreground = color

Groovy has a similar syntax I won't illustrate here. So why doesn't Ruby support this, or perhaps why doesn't JRuby automatically support this syntax for Java types?

To answer, let's take a look at what it would take to add this to current Ruby with only existing features.

The equivalent syntax in Ruby or JRuby might look like:

SomeClass.new(text, :bounds => bounds, :foreground => foreground)

...or possibly using a block as in:

SomeClass.new(text) { @bounds = bounds, @foreground = foreground }

However there's no existing accomodation in the semantics of Ruby for these syntax to work out of the box. It would not be hard to write a short bit of code to allow constructing objects in this fashion, of course (using the block approach as an example):

class Class
def construct(*baseargs, &initializer)
x = self.new(*baseargs)
x.instance_eval(&initializer) if initializer
x
end
end

...which would allow the block example above to work fine (with "construct" in place of "new"). For proxied Java objects, which don't actually have ruby instance vars (@whatever above) it would have to be a slightly different call:

JLabel.construct(text) { self.bounds = bounds, self.foreground =
foreground }

This works fine, but it's perhaps a little less beautiful. What about this impl instead?

class Class
def construct(*baseargs)
x = self.new(*baseargs)
yield.each_pair { |k,v| x.send("#{k}=", *v) } if block_given?
x
end
end

Which would allow a variation:

JLabel.construct(text) {{ :bounds => bounds, :foreground =>
foreground }}

(the double {{}} is intentional; the block returns a hash of initializers)

The bottom line is that this kind of syntactic sugar in other languages can easily be added to Ruby through various techniques, and so features like field-initializing constructors don't need to be part of the core language or any of the implementations.

Does anyone still wonder why I love this language?

4 comments:

  1. What's wrong with:

    def initialize(text, options = {})
      @text = text
      options.each do |k,v|
        self[k] = v
      end
    end

    Once that's set up you can call

    JLabel("text", :bounds => bounds, :foreground => foreground)

    and it Just Works (assuming you've got an implementation of []= that does the right thing for your class).

    ReplyDelete
  2. Ruby 1.9 has alternative syntax for hashes:

    key:value

    which is equivalent to:

    :key => value

    iirc ruby 2.0 should have support for keyword arguments

    Maybe it's worth to be ahead of some Ruby 2.0 features and ask Matz which syntax to choose...

    ReplyDelete
  3. Um... my point was that the code sample I gave implements something that you said in your post wasn't supported by Ruby.

    Or am I missing something?

    ReplyDelete
  4. piers: Your example would only work for one class: the one for which you redefined initialize. The example I gave adds general-purpose field-initializing constructor behavior for all types without modifying anything about those types. Your version works great for classes for which it's ok to redefine initialize (or for which you are defining initialize yourself). However if you want a general-purpose solution that doesn't modify existing classes, you need something more.

    ReplyDelete