Saturday, August 11, 2007

And this week, Prototypes, The Conclusion

Last time I followed a path I was put on by the excellent work of Ara T. Howard, and I stopped at a very naive implementation of the Prototype Programming Paradigm. (Yet another PPP;).

It was the fast (and late) conclusion of some days of refelection and testing of ideas, it is far from ideal...
The thing that disturbes me most is that I tempted to deal with data ( c.f. Prototype#deepdup), and I did not even do it well.
Dealing with data is a bad idea anyway, we are talking behavior here, and data should not influence the conceptional decisions to be taken.

Freed from any data concerns I came up with the following code, but before throwing it at you, gentle reader, I would like to comment the concept I am following here.
In order to avoid unneccessary confusion (I will not avoid necessary confusion, those who read my posts at ruby-talk know why ;), I will use Prototype (capitalized) whenever I refer to the implementation ( a Ruby class ), and prototype (lowercase) whenever I refer to a prototyped object (an instance of Prototype as a matter of fact).

And What Is A Prototype Anyway?

A prototype is an object that has its behavior closely attached to itself. It playes the role of a class and an instance of that class at the same time.
Remember, it was the main motivation of my last post to get rid of classes at all in our programs.
I losely follow a definition of prototype as defined here.

It is the Prototype class, yes we use classes for implementation of this nevertheless ;), that will take care of gluing behavior into the prototype object.
The basic instrument to do so is the __proto__ attribute Prototype defines for its instances.
We will see shortly that __proto__ is indeed a Module that will be included into new modules replacing it as soon as a prototype will be extended with new functionality.

It is important to remember that Ara T. Howard and Pit Capitain had to do very much work (as interesting and sophisticated it was) to allow for dynamic method redefinition for class based methods.
Our classless approach rips this burden off our shoulders of course, all methods a prototype responds to are module based (via the standard Ruby extend).

Inheritance & Method Resolution Order

Inheritance is therefore implemented by method inclusion into the __proto__ attribute of a prototype.

We have a prototype p which has stored its behavior in its __proto__ attribute. This is indeed an invariant of this PPP implementation:
A prototype is always extended by the module kept in its __proto__ attribute.
Whenever we want to extend a p with some new behavior it is done via a new module, either given as a module or by constructing one on the fly from a given block.
This new module will become the value ot the p's __proto__ attribute but not before including the value of the old value of the prototype's __proto__ attribute into the module. Thus the old behavior is not lost.
The code for doing this is factorized into Prototype's private extend_one method and is simple enough:

  def extend_one mod
proto = __proto__
mod.instance_eval { include proto } if proto
self.__proto__ = mod
extend mod
end


As mentioned above the Prototype class implements an attribute __proto__. The constructor allows us to indicate behavior to be inherited as follows, a list of modules ( of whihch the last will be the last in the inheritance chain ) and an optional block of which a module will be constructed.
The module constructed from the block is the last to be inherited into the __proto__ chain.

The following example shows the method resolution order as described above.


   module A
def a; 42 end
def b; 46 end
def c; 39 end
end
module B
def b; 52 end
end
p = Prototype.new( A, B ){
def c; 60 end
}

puts p.a # --> 42
puts p.b # --> 52
puts p.c # --> 60

And here goes the definition of Prototype#initialize which does the work to arrive at the result shown above:


PrototypeError = Class::new StandardError

class Prototype
attr_accessor :__proto__
def initialize *args, &blk
ext_proto *args
ext_proto Module::new( &blk ) if blk
end # def initialize *args, &blk
...
end # class Prototype



How To Use It, The Prototype API

We want of course some simple way to create prototypes new clones or copies of a prototype or to extend prototypes.
The following interface methods can be used for this task:


class Prototype

def ext_proto *args, &blk
args.each do
| arg |
case arg
when Module
extend_one arg
else
begin
extend_one arg.__proto__
rescue NoMethodError
raise PrototypeError,
"Cannot extend an object with a non Module or non Prototype object"
end
end # case arg
end # args.each
extend_one Module::new( &blk ) if
blk or args.empty?
self
end # def ext_proto
end # class Prototype

module Kernel
def new_proto superobject=nil, *mods, &blk
begin
superobject.dup
rescue TypeError, NoMethodError
Prototype.new
end.ext_proto( *mods, &blk )
end
end # module Kernel

Thus we will be allowed to create a new prototype with


   new_proto nil, Comparable do
def <=> other
...
end
end

Or we might like to write more explicit code:



module Printable
def to_s; inspect end
end

sortable = new_proto
sortable.ext_proto Printable, Comparable do
def <=> other
name <=> other.name
end
attr_accessor :name
def init name
self.name = name
end
end

puts [sortable.new( "Vilma" ),
sortable.new( "Angie" ) ].sort
##<Prototype:0x2b64110 @name="Angie", @__proto__=#<Module:0x2b643f4>>
##<Prototype:0x2b641c4 @name="Vilma", @__proto__=#<Module:0x2b643f4>>


No More Classes! No More Modules?

Although I do not see any technical problem to use modules directly, it might be more consistent an approach not to do so anywmore.
We have got rid of class why not get rid of module too? This kind of paradigm change can do a lot to your programming style.

The alternative would be to use prototypes and anonymous module blocks exclusively (still being able to do a "include mod" in the block of course).
In this alternative way the code above would look as follows:


printable = Prototype.new {
def to_s; inspect end
}
sortable = new_proto printable, Comparable do
...

Note that we still allow modules in the inheritance chain, this seems quite useful for the usage of predefined modules.

And What About Object Initialization?
This is the last missing piece of our puzzle, Prototype defines a new method that will call an init method for the newly created object, if it exits that is of course.
Not much magic here


class Prototype
def new *args, &block
o = dup
o.extend __proto__
o.instance_eval {
init( *args, &block )
} if respond_to? :init
o
end # def new *args, &block
end # class Prototype

Please note that our only concern is to disassociate the new object from the generating prototype object. It is completely up to the user to manage the data, data can be shared between a generator object and a generated object by means of references pointed to by instance variables defined for the generator.
Example for shared data between generator prototype and generated prototype:


   printable = Prototype.new {
def to_s; inspect end
}

dog = new_proto printable do
attr_accessor :name
def init name=nil
self.name = name if name
end
end
vilma = dog.new %q{vilma}
angie = vilma.new
angie.name << "& angie"

puts vilma
puts angie
##<Prototype:0x2b64304 @__proto__=#<Module:0x2b64458>, @name="vilma& angie">
##<Prototype:0x2b6428c @__proto__=#<Module:0x2b64458>, @name="vilma& angie">


The PPP Implementation
Here goes the whole implementation:


PrototypeError = Class::new StandardError

class Prototype
attr_accessor :__proto__
def initialize *args, &blk
ext_proto *args
ext_proto Module::new( &blk ) if blk
end # def initialize *args, &blk

def ext_proto *args, &blk
args.each do
| arg |
case arg
when Module
extend_one arg
else
begin
extend_one arg.__proto__
rescue NoMethodError
raise PrototypeError,
"Cannot extend an object with a non Module or non Prototype object"
end
end # case arg
end # args.each
extend_one Module::new( &blk ) if blk or args.empty?
self
end # def ext_proto

def new *args, &block
o = dup
o.extend __proto__
o.instance_eval {
init( *args, &block )
} if respond_to? :init
o
end # def new *args, &block

private
def extend_one mod
proto = __proto__
mod.instance_eval { include proto } if proto
self.__proto__ = mod
extend mod
end
end

module Kernel
def new_proto superobject=nil, *mods, &blk
begin
superobject.dup
rescue TypeError, NoMethodError
Prototype.new
end.ext_proto( *mods, &blk )
end
end # module Kernel


Performance?

Performance is bad of course, Prototype based code is about 10 times slower than class based code, as my first benchmarks have shown.

1 comment:

Anonymous said...

How fast is Boa?

http://math.andrej.com/2008/05/07/an-object-oriented-language-boa/