InfoQ

Article

Adding Properties to Ruby Metaprogramatically

Posted by Werner Schuster on Apr 18, 2007 01:06 PM

Community
Ruby
Topics
Programming ,
Domain Specific Languages
Tags
Language Features ,
Metaprogramming

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.

Good article, but many errors. by Jules Jacobs Posted Apr 19, 2007 12:57 PM
Working on fixes and article will be updated. by Obie Fernandez Posted Apr 20, 2007 10:10 AM
Re: Good article, but many errors. by Werner Schuster Posted Apr 20, 2007 10:18 AM
Extending Modules by Werner Schuster Posted Apr 20, 2007 10:48 AM
Re: Extending Modules by Jules Jacobs Posted Apr 20, 2007 11:23 AM
The article has been re-published with corrections. by Obie Fernandez Posted Apr 20, 2007 12:50 PM
Listeners and predicate by Victor Cosby Posted Apr 23, 2007 5:20 PM
Re: Listeners and predicate by Victor Cosby Posted Apr 24, 2007 10:56 AM
  1. Back to top

    Good article, but many errors.

    Apr 19, 2007 12:57 PM 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.

  2. Back to top

    Working on fixes and article will be updated.

    Apr 20, 2007 10:10 AM by Obie Fernandez

    Thank you for pointing out improvements!

  3. Back to top

    Re: Good article, but many errors.

    Apr 20, 2007 10:18 AM 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!

  4. Back to top

    Extending Modules

    Apr 20, 2007 10:48 AM 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.

  5. Back to top

    Re: Extending Modules

    Apr 20, 2007 11:23 AM 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.

  6. Back to top

    The article has been re-published with corrections.

    Apr 20, 2007 12:50 PM by Obie Fernandez

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

  7. Back to top

    Listeners and predicate

    Apr 23, 2007 5:20 PM 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.

  8. Back to top

    Re: Listeners and predicate

    Apr 24, 2007 10:56 AM 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
    

Educational Content

Bindings, Platforms, and Innovation

This presentation focuses on the Internet and separating myth from fact, history from the future, and the mundane from the imaginative. Bob Frankston presents a vision of what could and should be.

Orchestrating Long Running Activities with JBoss / JBPM

This article explores the use of JBoss and jBPM to implement design solutions that effectively address the issue of orchestrating long running activities.

Neo4j - The Benefits of Graph Databases

This presentation covers the use of graph databases as an optimal solution for data that is difficult to fit in static tables, rapidly evolving data or data that has a lot of optional attributes.

Realistic about Risk: Software development with Real Options

This session introduces Real Options and shows how it can help in running your project. Real Options is a decision-making process that can be used to manage risk.

Communication Flexibility Using Bindings

This article discusses the use of bindings on services and references (including the instance of non-configured bindings) as the means to implement SCA communications in a Web and SOA environment.

Writing DSLs in Groovy

After a short introduction to DSLs, Scott Davis plays with the keyboard showing how to approach the creation of a DSL by typing working snippets of Groovy code that get executed.

Scaling Agile with C/ALM (Collaborative Application Lifecycle Management)

IBM Rational and InfoQ present, Scaling Agile with C/ALM, an eBook showing organizations how to become “finely tuned software delivery machines” by enabling team integration and scaling.

Concurrent Programming with Microsoft F#

Amanda Laucher presents a real life enterprise application written in F#. She shows actual code snippets, explaining design decisions and suggesting how to use some of the F# constructs.