InfoQ Homepage News Article: Adding Properties to Ruby Metaprogramatically

# Article: Adding Properties to Ruby Metaprogramatically

We're proud to publish a feature article by InfoQ's own Werner Schuster, where he walks us through a very simple and familiar example of adding Java-style properties support (declarative getters, setters and change listeners) to Ruby classes. He does so via a Mixin and using elements of Ruby meta-programming. Taking the idea a step forward, he gently guides the reader into usage of design-by-contract principles and gives a teasing taste of what a pluggable type system might look like in a Ruby setting.

If you're coming over to Ruby from the Java world, and want a good primer on what metaprogramming is all about, then definitely read Adding Properties to Ruby Metaprogramatically today.

Style

## Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

• ##### Good article, but many errors.

by Jules Jacobs /

• ##### Working on fixes and article will be updated.

by Obie Fernandez /

• ##### Re: Good article, but many errors.

by Werner Schuster /

• ##### Extending Modules

by Werner Schuster /

• ##### Re: Extending Modules

by Jules Jacobs /

• ##### The article has been re-published with corrections.

by Obie Fernandez /

• ##### Listeners and predicate

by Victor Cosby /

• ##### Re: Listeners and predicate

by Victor Cosby /

• ##### Good article, but many errors.

by Jules Jacobs /

Your message is awaiting moderation. Thank you for participating in the discussion.

define_method("#{sym}".to_sym) {
instance_variable_get("@#{sym.to_s}")
}

Why do you convert sym to a string and then back to a symbol? It's more common to use do...end for blocks where the open brace is not on the same line as the end brace.

define_method("#{sym}=".to_sym) {|value|
instance_variable_set("@#{sym.to_s}", value)
}

You don't need to convert the thing to a symbol. define_methods accepts strings too. You don't have to use sym.to_s either as it's already converted to a string by string interpolation. Same thing for do...end.

define_method(:direction=){|value|
instance_variable_set("@#{sym.to_s}", value)
fire_event_for(:direction)
}

Why do you have :direction= and :direction in there, but also #{sym.to_s}? This code will not work.

@listener[:sym] << x
}

to_sym is not necessary here. @listener[:sym] should be @listener[sym].

"The code for setting up the listener lists and the rest is left to the reader as exercise. (Stop moaning, it's just 3 lines of code)."

Could you give these 3 lines? I don't know how to do it (without cheating by putting multiple things on one line). The fire_event_for will be at least three lines of code. Setting up the listener hash table is also hard with this implementation. So again: please show the code :)

h = CruiseShip.new
h.add_bar_listener lambda {|x| puts "Oy... someone changed the property to #{x}"}
h.hello = 10

Where do you define the class Listener? Where does add_bar_listener come from? Where does hello= come from? Why does the bar_listener get called if you call hello=(10)?

class Ship
extend Properties
end

Maybe it's good to explain that extend adds the methods to the Ship class object and not to the Ship instances? This is not the common way to use mixins in Ruby. Well maybe it's better to pretend that it's this simple.

property :speed {|v| v >= 0 && vn< 300 }

This code won't parse. You need parens here.

def property(x, &predicate)
define_method("#{sym}=".to_sym) {|arg|
if(!predicate.nil?)
if !predicate.call(arg)
return
end
end
instance_variable_set("@#{sym.to_s}", arg)
prop_holder = instance_variable_get("@#{sym.to_s}_property_holder")
prop_holder.fire_event(arg) if prop_holder
}
end

OK. to_sym not necessary. "!predicate.nil?" is the same as "predicate". Why do you use braces for the first if but not for the second? to_s is redundant (2x). Where do you set @#{sym}_property_holder? What is prop_holder? What is prop_holder.fire_event?

property :speed, in([0..300]

Missing close paren ;-). Why do you put the range in an array?

property :speed, in(0..300)

def in(x)
lambda{|value| x === value}
end

This will allow you to pass a regex:

property :email, in(/email regex/)

But the name "in" isn't very good now. That's not a problem because you cannot use "in" anyway because it's a reserved word.

I hope you fix the errors. This is a great article if you do.

• ##### Working on fixes and article will be updated.

Your message is awaiting moderation. Thank you for participating in the discussion.

Thank you for pointing out improvements!

• ##### Re: Good article, but many errors.

Your message is awaiting moderation. Thank you for participating in the discussion.

Wow, thanks for the detailed comments Jules!
I guess it serves me right for copy/pasting code between evolving source code and an evolving article text - nothing good can come from that.
The more stupid errors will be fixed right away;

• extending a Module
If I _include_ a Module, the methods will be added as instance methods, but to have
something like this
class Foo
property :xyz
end
they need to be available as class methods. With include, you'd call them as instance
methods so... I guess you'd have to setup the properties in the constructor or somewhere else. If I'm missing a better way to do this, I'd be interested to hear;
• the do/end vs braces thing...
well, I don't know, I like the braces; there's a precedence difference between the two, but otherwise it seems to be a matter of taste... also: the word "do" feels like clutter, and I wanted to make the solution seem as clean as possible; There's no real reason why there's a difference for delimiting blocks - Smalltalk uses brackets for _all_ blocks;
• thanks for the === tip;
• good catch on the "in" keyword... I updated the article to mention this; however, the ideas in this section were thought of as possible notations, that the user could play with (as always with internal DSLs, it's good to come up with the look, then chip away at it and prop some parts up to make it Ruby code);.

Again... thanks for keeping me honest, Jules!

• ##### Extending Modules

Your message is awaiting moderation. Thank you for participating in the discussion.

This include vs extend issue made me curious... I looked into Rails (actually ActiveRecord) and how it does it's has_many etc methods, and it also uses extend (grep the Rails source code for "extend" or "base.extend"). Again, if you want the fancy, declarative look, this seems to be the only way.

• ##### Re: Extending Modules

by Jules Jacobs /

Your message is awaiting moderation. Thank you for participating in the discussion.

Here's a nice trick (I think Rails uses a similar thing):

module Example  def instance_method    # this method will be added to the class as an instance method  end  module ClassMethods    def class_method      # this method will be added to the class as a class method    end  end  def self.included(klass)    klass.extend(ClassMethods)  endendclass Test  include Example  # this will call the Example.included callbackend

If you need only class methods just using extend in the class that needs the Properties seems cleaner.

• ##### The article has been re-published with corrections.

Your message is awaiting moderation. Thank you for participating in the discussion.

Thanks to alert reader Jules Jacobs for pointing out areas for improvement.

• ##### Listeners and predicate

by Victor Cosby /

Your message is awaiting moderation. Thank you for participating in the discussion.

Here's one way to setup the listeners...

module Properties    def self.extended(base)     base.class_eval %q{def fire_event_for(sym, *arg) @listener[sym].each {|l| l.call(arg) } end }  end    def property(sym, &predicate)        ...      define_method("add_#{sym}_listener") do |x|       @listener ||= {}      @listener[sym] ||= []      @listener[sym] << x    end  end

but I'd love to know how you've gotten the predicate business to work.
property :speed {|v| v >= 0 && vn< 300  }
won't even parse.

• ##### Re: Listeners and predicate

by Victor Cosby /

Your message is awaiting moderation. Thank you for participating in the discussion.

Ah, looking through Jules's comments, it looks like there was one other error that hasn't been corrected. So here's one implementation. I love it that with Ruby it takes only a minor method signature to property to support the block vs. lambda switch. (I wonder: is there is a way to get the method to support both with a single signature?)

module Properties    def self.extended(base)     base.class_eval %q{def fire_event_for(sym, arg) @listener[sym].each {|l| l.call(arg) } end }  end    # def property(sym, &predicate)  def property(sym, predicate=nil)    define_method(sym) do      instance_variable_get("@#{sym}")    end      define_method("#{sym}=") do |arg|      return if !predicate.call(arg) if predicate      instance_variable_set("@#{sym}", arg)      fire_event_for(sym, arg)    end      define_method("add_#{sym}_listener") do |x|       @listener ||= {}      @listener[sym] ||= []      @listener[sym] << x    end        define_method("remove_#{sym}_listener") do |x|       @listener[sym].delete_at(x)    end  end    def is(test)    lambda {|val| test === val }  endendclass CruiseShip  extend Properties    property :direction  # property(:speed) {|v| v >= 0 && v < 300 }  property :speed, is(0..300)endh = CruiseShip.newh.add_direction_listener(lambda {|x| puts "Oy... someone changed the direction to #{x}"})h.direction = "north"h.add_speed_listener(lambda {|x| puts "Oy... someone changed the speed to #{x}"})h.add_speed_listener(lambda {|x| puts "Yo, dude... someone changed the speed to #{x}"})h.speed = 200h.speed = 300h.speed = 301h.speed = -1h.speed = 2000puts h.directionputs h.speedh.remove_speed_listener(1)h.speed = 200h.speed = 350puts h.directionputs h.speed

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Is your profile up-to-date? Please take a moment to review and update.

Note: If updating/changing your email, a validation request will be sent

Company name:
Company role:
Company size:
Country/Zone:
State/Province/Region:
You will be sent an email to validate the new email address. This pop-up will close itself in a few moments.