BT
x Your opinion matters! Please fill in the InfoQ Survey about your reading habits!

Adding Properties to Ruby Metaprogramatically

Posted by Werner Schuster on Apr 18, 2007 |

Properties... the next frontier. Well, at least if you can't stop yourself from watching the Java blog space, which is abuzz with discussions about this topic. Are Properties the next feature to save the world, give us that so desperately needed silver bullet, and finally make Java developers feel good in their skins again? Bah... it's boring to just theorise about the superpowers of Properties. How about we see if Properties can be useful by actually adding them to Ruby and see how they fare... and don't worry: no Lexers, Grammars, or Language Specs were harmed during the production of this article.

Embedded DSL

How could we add Properties to Ruby? Well, let's try to implement an Embedded DSL. The best way to start a DSL is to doodle around and sketch something that looks right.

Let's see... C# properties in Ruby would look something like that:

class CruiseShip
property direction
property speed
end

With the basic look done, we see that this is not quite legal Ruby code, but it's not far off. If you load this class, Ruby will complain about "direction" and "speed" being unknown.

Let's give the property call a property name in a way that doesn't get evaluated on load. Yes: Symbols to the rescue!

class CruiseShip
property :direction
property :speed
end

The code parses and loads... but it still throws an error: NoMethodError: undefined method 'property' for CruiseShip:Class

To solve this we use a simple fact: Ruby class definitions are more than just declarations, they are actually executed at load time. When this line is loaded:

 property :direction 

a function with name "property" is searched and then called with parameter :direction. How do we get the property method in there? Well, for now, let's just do this:

def property(sym)
# do some stuff
end

class CruiseShip
property :direction
property :speed
end

And there we are: the code loads without complaints. The property definition on top of the file isn't pretty, but we'll deal with that later and make it reusable.

Let's flesh out the code in the property function. property is called when the class definition is executed, which means it can add methods to the class. With this, we can define a method, which will then be part of the class. Add this code to the property function:

define_method(sym) do
instance_variable_get("@#{sym}")
end

This causes the equivalent of this code to be added to the class:

def direction
@direction
end

This is the getter for the direction property, the code for the setter can be added in the same way:

define_method("#{sym}=") do |value|
instance_variable_set("@#{sym}", value)
end

This isn't particularly useful yet... actually, the same thing could have been done with attr_accessor :property which is already available in Ruby.

What? All this text and then the feature we implemented is already available? Not, quite. With Properties we want more than just adding setters/getters on a class. Properties should make it possible to register listeners that get notified when the value of a property changes. After all, the trusty old Observer pattern has served us well. Except, that using it in Java always involves a lot of tedious typing and boilerplate code. There's the code that actually calls all the listeners, which is always the same... but before the notification comes the registration, which consists of defining add/remove methods that allow to register/unregister listeners. Since this code is specific to the property names (you want add_direction_listener,...), there is no way to have an automatic way to do this in Java.

But wait! This is a Ruby, which lends itself well to MetaProgramming, the art of having that lazy bum of a computer do the boring work for us, leaving us more time to smell the roses and feed the cat.

There is already some code that implements the setter

define_method("#{sym}=") do |value|
instance_variable_set("@#{sym}", value)
end

Well... couldn't that just send out the notifications as well?

define_method("#{sym}") do |value|
instance_variable_set("@#{sym}", value)
fire_event_for(sym)
end

The other bit missing here are the listener management methods. Here we just use the same define_method trick again, ie. in the property function, define a method for this particular event:

define_method("add_#{sym}_listener") do |x| 
@listener[sym] << x
end

Methods for removing or accessing listeners would be handled the same way. The code for setting up the listener lists and the rest is left to the reader as exercise. (Stop moaning, it's just a few lines of code each)

Using the code

Let's see how all this works.

h = CruiseShip.new
h.add_direction_listener(Listener.new)
h.add_bar_listener lambda {|x| puts "Oy... someone changed the property to #{x}"}
h.bar = 10

This will output: "Oy... someone changed the property to 10"

Dandy... that was easy. But before we continue we some more fun, let's do a bit of cleaning.

Wrap it up an tie a bow around it

Now... how do we get all this inside a class? This is where a very useful feature comes in: Mixins. These are just ordinary Ruby Modules that are mixed into anclass definition. Sounds odd? Nope, it's as simple as this:

class Ship
extend Properties
end

This will make all functions from the Properties Module available from inside Ship. This is how the simple notation for the property call works. Other languages would have to resort to Inheritance for this, ie. define the method in a class and force users to extend that. With Mixins, the class hierarchy stays clean, and you can just mix in (oh... that's where the name comes from!) the features you need.

This means, that we can move the code in our demo code to its very own Mixin:

module Properties
def property(sym)
# all the nice code
end
end

This is why you can do this

class Ship
extend Properties
property :direction
property :speed
end

This has the nice effect that it also records the fact that this class is using the Properties Mixin. When reading this code, if you're not yet familiar with this enhancement yet, just look at eithern the documentation for the Properties Mixin or look at its source.

Let's have some fun

With the basic Property goodies in place, let's have some fun. The concept of Design by Contract allows you to define some constraints and invariants of your class. Statically typed languages have a primitive version of this:

void foo(int x) 

means that only int values can be passed in. Of course... what exactly is an int? Why are values in a range of 2^31 so interesting, particularly if we actually want the :speed property to accept only values in a range of 0..300 (this ain't your average cruise sheep...). Another way of looking at this are Gilad Bracha's idea of Pluggable Type Systems, ie. instead of relying on often inadequate existing type systems, simply define your own and allow to define type ranges in a more declarative way, instead of having to clutter the code with lots of if/else constructs and defensive programming.

So, why the lecture on DbC and Pluggable Type Systems? Well, since we're busy extending the language, let's add something useful, like a feature for specifying constraints on the property values.

This is where you can get creative and decide how you want this to look. This can take a range, or any other named type. You could also throw in some predicate

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

The implementation for this simple:

def property(x, &predicate)
define_method("#{sym}=") do |arg|
if(predicate)
if !predicate.call(arg)
return
end
end
instance_variable_set("@#{sym}", arg)
fire_event_for(sym)
end
end

This does leave the nice side effect that the value of the property will always be in a defined range inside the class, without having to clutter the code with range, type or nil checks. You just define all specific constraints of the property in one central space, and be done with it.

Actually, this speed constraint could be defined in a much more concise way. This is outside the scope of this article, but as an exercise, try to implement something like this:

property :speed, in(0..300) 

Note that this is not legal Ruby code (in is a Ruby keyword). It's useful to start from the way we want the code to look and then proceed to make it Ruby code.

Enjoy.

Conclusion

Of course, the latter part was not something that would get added to a Properties feature, simply because this kind of typing should be unrelated to the Property and Notification features. But in Ruby, you can just mold the language around your needs... this is just an example of what's possible.

Arguments against Embedded DSLs are manifold. Like the idea that they make code harder to read. And it's true. Just like this is difficult to read: print(x). How in the world could you know what this function call does? Well... you could read the documentation or peek at it's source code. But how is this different from the DSL implementation? It isn't.

The techniques used here are very simple, and every developer who doesn't get scared of words such as Polymorphism or Recursive Type Realization (or whatever 100 U$ words are popular now) will understand them in a moment, and they make the code so much more concise, to the point and maintainable.

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.

Tell us what you think

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

Email me replies to any of my messages in this thread

Good article, but many errors. by Jules Jacobs

The subject of this article is good,but the code is full of bugs and style errors.

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.

define_method("add_#{sym}_listener".to_sym){|x|
@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_direction_listener(Listener.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. by Obie Fernandez

Thank you for pointing out improvements!

Re: Good article, but many errors. by Werner Schuster

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;
Let me address your questions:

  • 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 by Werner Schuster

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

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

class Test
include Example # this will call the Example.included callback
end


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. by Obie Fernandez

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

Listeners and predicate by Victor Cosby

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

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 }
end
end

class CruiseShip
extend Properties

property :direction
# property(:speed) {|v| v >= 0 && v < 300 }
property :speed, is(0..300)
end

h = CruiseShip.new

h.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 = 200
h.speed = 300
h.speed = 301
h.speed = -1
h.speed = 2000

puts h.direction
puts h.speed

h.remove_speed_listener(1)

h.speed = 200
h.speed = 350

puts h.direction
puts h.speed

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

Email me replies to any of my messages in this thread

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

Email me replies to any of my messages in this thread

8 Discuss

Educational Content

General Feedback
Bugs
Advertising
Editorial
InfoQ.com and all content copyright © 2006-2014 C4Media Inc. InfoQ.com hosted at Contegix, the best ISP we've ever worked with.
Privacy policy
BT