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?

7 comments:

Anonymous said...

Another pattern, used a lot in Rails...

http://pastie.caboo.se/15285

Anonymous said...

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).

Anonymous said...

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...

Charles Oliver Nutter said...

anon: Yes, I had another version that did that, but it's obviously more verbose. I wanted to demonstrate how with a few clever tricks you can actually appear to change the language syntax. The first version I showed is mostly the same as the pattern you provided, except that the block is evaluated within the new object to avoid repeated "label" dereferencing.

piers: That works too, and it's another option. I went with the block approach to avoid potentially messing up constructors that have restargs. Obviously my versions won't work with constructors that take a block, but it's just a demonstration of the potential.

lopex: As far as I've seen, there's no plans to include field-initializing constructors, though you're right that there will be syntax added that might more easily support them. However Ruby 2.0 is off in the distance, and I wanted to show what folks could do today.

Anonymous said...

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?

Charles Oliver Nutter said...

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.

Anonymous said...

Oh, silly me. Guess who didn't notice you were a method on class.