Article: Adding Properties to Ruby Metaprogramatically
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.
Good article, but many errors.
by
Jules Jacobs
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
Re: Good article, but many errors.
by
Werner Schuster
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
Re: Extending Modules
by
Jules Jacobs
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
Listeners and predicate
by
Victor Cosby
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
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
Concurrency in Clojure
Stuart Halloway May 17, 2013
Confessions of an Agile Addict
Ole Friis Østergaard May 16, 2013
Web Development: You're Doing It Wrong
Stefan Tilkov May 16, 2013
Programming The Feynman Way
Ben Evans May 15, 2013





Hello stranger!
You need to Register an InfoQ account 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