By looking at our examples however, we see that we use instance variables for properties (or attributes) of our objects to remain in the OO jargon.
By defining attribute accessors this fact can be hidden but we can by no means avoid that instance variables are used directly to access the properties we have defined.
This week we will add a feature to the Prototype implementation that allows us to define properties without any usage of instance variables. We will define properties by means of closures, and we will allow a very simple way to define properties on a Per Object base as well as on a Per Prototype base.
Properties
Properties contain information attached to an object. In pure Ruby we use instance variables very often to represent properties. As a matter of fact we are talking about attributes and they are implemented with the well known attr_*
definitions:
class Person
attr_reader :name
attr_accessor :age
def initialize name, age=nil
@name = name
@age = age if age
end
end
spencer = Person.new "Tracy", 107
hepburn = Person.new "Katharine"
Obviously name and age are properties attached to each object and not shared amongst all objects of the same class. If we want such a beast in pure Ruby we can use Class Instance Variables1.
The classical example of such a shared property is to count instances of the class, and that is what the following code does:
class Person
attr_reader :name
attr_accessor :age
@count = 0
class << self
attr_accessor :count
end
def initialize name, age=nil
@name = name
@age = age if age
self.class.count += 1
end
end
spencer = Person.new "Tracy", 107
puts Person.count # -> 1
hepburn = Person.new "Katharine"
puts Person.count # -> 2
(1)The usage of Class Variables (starting with @@) has been discussed a lot on the Ruby-Talk ML and I adhere to a vast group of Rubyist who discourage their usage.
Can PPP do this too?
Well it can, now, but you have not seen the code yet;). Let us have a look how it is used before explaining the implementation. The code below is the equivalent to the above code but expressed in PPP.
require 'ppp'
person = new_proto {
proto_variable :count, 0
def init name, age=nil
obj_variable :name, name
obj_variable :age, age
self.count += 1
end
}
spencer = person.new "Tracy", 107
puts spencer.count # -> 1
hepburn = person.new "Katharine"
puts person.count # -> 2
There are some differences, as a matter of fact per object variables and per prototype variables are syntactically identical, please note too, that, like in Pure Ruby, you cannot assign to a prototype without an explicit receiver, hence the self.count += 1 above, the same has to be done for object variables:
person.ext_proto {
def birthday
self.age += 1
end
}
Looking under the Hood
I decided to use closures, I just dislike the @ sign I guess;)
class Object
def singleton; class << self; self end end
end
First of all I monkey-patched Object with the singleton method above, this is used very frequently by many Metaprogramming Gurus, so I need that too. Seriously you will see soon why this is useful.
class Prototype
def obj_variable varname, value, readonly = false
variable = value
singleton.instance_eval do
define_method varname do variable end
end
return if readonly
singleton.instance_eval do
define_method "#{varname}=" do | val | variable = val end
end
end # def obj_variable varname, value, readonly = false
end
Then the definition of obj_variable goes right into the Prototype class, that is why we use it in the init method and not inside the prototype block.
Please note the usage of the closure. The local variable called variable is created in the first line of the method and then we use two metaprogramming tools, instance_eval and define_method to define the accessor methods to the property. As both of these use blocks we have created a closure to access the local variable variable. Nobody can take this away from us anymore.
class Module
def proto_variable varname, value, readonly=false
variable = value
define_method varname do variable end
return if readonly
define_method "#{varname}=" do |val| variable = val end
end # def proto_variable varname, value
end # class Module
The definition of proto_variable has to go into Module of course. That is the context in which the block of new_proto or ext_proto is interpreted in.
Note the same technique as above only that we do not need the singleton here, we want the method to be defined in the prototype itself as this behavior is shared between the prototype and its objects.
Is This Good For Something?
Sure, allows me to write a Blog :). But maybe I can convince you of the Beauty and Usefulness of the PPP by means of the example of the open-proto implementation.
The open-proto implementation mimics open-struct and Facet's open-object behavior. Properties can be created on the fly:
require 'labrador/exp/open-proto'
my_open = new_proto(Prototype::OpenProto) {
def greeting
puts "I am #{name} and I have a value of #{value}"
end
}
my_object = my_open.new :name => "Fourty Two"
puts my_object.name
my_object.value = 222
another = my_object.new :name => "Fourty Six", :value => 42
my_object.greeting
another.greeting
Properties are defined on the fly, on a per object base and either by assignment or in the constructor.
It amazed myself how easy it was to implement this behavior (as it is in Pure Ruby I admit ;).
class Prototype
OpenProto = new_proto do
def init params={}
params.each_pair do |k,v|
obj_variable k, v
end
end
def method_missing name, *args, &blk
super name, *args, &blk if
args.size != 1 or
blk or
name.to_s[-1] != ?=
obj_variable name.to_s[0..-2], args.first
end
end
end
I make this 17 lines of Ruby Code.
Conclusion:
I hope you have enjoyed the journey, gentle reader, if you would like to play around with this programming style maybe you would like to have a look at Labrador as PPP and open-proto are part of version 0.1 of the Lazy Programmer's Best Friend.
I have uploaded the two files labrador/exp/ppp and labrador/exp/open-proto in an extra file called labrador-experimental-0.1 you can download all that from Rubyforge.