Wednesday, August 1, 2007

Classless Ruby, a Paradigm Change?

What are classes for in Ruby?

That seems quite a stupid question at first, let us see if we can work to a point where the question makes more sense.

The whole thing started in my mind by a recent post of Ara T. Howard, implementing a neat meta-programming trick allowing to redefine methods.

Method Redefinition
The basic idea was to allow to redefine a method in a class by still having access to the redefined method via the super call.
Without looking at the implementation this is what Ara wanted to achieve.

The implementation, heavily influenced by Pit Captain, can be found here, have a look here if you are interested in Ruby Metaprogramming, it is really top notch code.

class C
def foo() 'f' end
def bar() 'b' end
def foobar() foo + bar end
end

c = C.new
p c.foobar # => "fb"

class C
redefining do
def foo() super + 'oo' end
end
end

p c.foobar # => "foob"

class C
redefining do
def bar() super + 'ar' end
end
end

p c.foobar # => "foobar"

class C
redefining do
def foo() super.reverse end
def bar() super.reverse end
end
end

p c.foobar # => "oofrab"

It is indeed a nice feature to have, a very modular inheritance mechanism. If you have looked at the implementation above, surely you have had a look at the implementation already;), you will see that it is rather complicated.
There are two reasons for that, firstly there is no way to tell Ruby to accept block parameters in a block, thus the following just will not be possible in Ruby 1.8, this will fortunately change in 1.9.


define_method( :my_method ) {
|*args, &blk|
...
}

And secondly there is a well defined behavior of directly defined methods being looked up before mixed-in methods.
When Ara published his redefining code first I had the urge to write this much, much simpler and I almost succeeded, actually I knew that the code was not doing what Ara wanted, but I felt that the simplicity of the solution was worth looking into it.
I also preferred to define the meta-code in class Module rather than in class Class.

A Naive Approach


  class Module
def redefine &blk
include Module::new( &blk )
end
end


Now this does some pretty neat tricks already, let us look at one example:
  module M
def m; 1 end
end

module N
include M
redefine do
def m; super + 1 end
end
end

class A
include N
redefine do
def m; super + 1 end
end
end

puts A.new.m # --> 3

Unfortunately we cannot redefine a method that has been declared in a class as it will be looked up first, that is why Ara and Pit went through so much trouble to save the method, remove it from the class and put it on a call stack, they did not do that (only) for fun.
In order to demonstrate it change the definition of class A as follows


  class A
def m; 41 end
redefine do
def m; super + 1 end
end
end

puts A.new.m


The deception of not getting the right answer is great of course.
But how can we get 42? Yes, of course, by using Ara's and Pit's code...
Actually I fell in love with my naive code above.
I am well aware of its limitations, but why not just continue walking on this road? We will see where that gets us.
I was quite pleased with the journey.


Mixin Rules, Or Does It Not?

The first idea to allow redefinition is simply to use redefine for the definition of the methods in the class.
This would also allow to explicitly forbid redefinition for conventional methods, the following example does not even use redefine as it is just too simple, we explicitly mix an anonymous module in.

  class Mixin
include Module::new {
attr_reader :a
def initialize a
@a = a
end
def increment
@a += 1
end
}
def old_style; "old_style" end
end # class Mixin

m = Mixin.new 46
puts m.a # --> 46
m.increment
puts m.a # --> 47
puts m.old_style # --> "old_style"

class Mixin
include Module::new {
def increment
super
@a += 2
end

def old_style
super << "::new_style"
end
}
end

m.increment
puts m.a # --> 50!!
puts m.old_style # --> "old_style"


Let us imagine, for the sake of argument, that you like this idiom, are you going to write the following code pattern forever?


class MyClass
include Module::new {
...


Probably not, why not mimic Ruby's class keyword a little bit and hide the implementation details in a Kernel method?

module Kernel
def new_style_class name, superclass=Object, &block
begin
Object.const_get( name )
rescue
Object.const_set( name, Class::new( superclass ) )
end.
instance_eval{ include Module::new( &block ) }
end
end # module Kernel

new_style_class :A do
attr_reader :value, :version
def initialize
@version=1
@value =0
end

def set_value v
@value = v
end
end

a = A.new
puts a.version
a.set_value 52
puts a.value

new_style_class :A do
def initialize
@version=2
end
end
puts A.new.version


Note that the such created new_style_class is a straight forward Ruby class that you can use and monkey patch as any other Ruby class.

Did I Say Classless?

Easy enough, let us just forget about Class.

Instead of creating the classic Ruby class and storing it in the class constant, why not just creating objects with a Kernel method, a little bit like in Javascript.

Then we extend them with our anonymous modules on the fly.

I will finish this post with some code demonstrating the usage of classless Ruby but please note how Ara's brilliant idea is still there, just look into Object#ext_object.

PrototypeError = Class::new( StandardError )
class Prototype
attr_accessor :__proto__
class << self
alias_method :orig_new, :new
def new *args, &block
o=orig_new( *args, &block )
o.__proto__ = Module::new
o
end
end

def deepdup
o = dup
instance_variables.each do |ivar|
val = instance_variable_get( ivar )
val = val.dup rescue val
o.instance_variable_set( ivar, val )
end
o
end
end # class Prototype

module Kernel
def new_object superobject=nil, &block
begin
superobject.deepdup
rescue NoMethodError
Prototype.new
end.ext_object( &block )
end
end # module Kernel

class Object
def ext_object &block
raise PrototypeError,
"Not a prototype" unless
respond_to?( :__proto__) && Module === __proto__
old_proto = __proto__
self.__proto__ = Module::new( &block )
__proto__.instance_eval{ include( old_proto ) }
extend __proto__
end

def clone &block
new_object( self, &block )
end
end # class Object

my_object = new_object {
attr_accessor :a, :b
def to_s; "#@a, #@b" end
}
my_object.a = 52
my_object.b = 60
other = my_object.clone {
def reset; @a=@b=nil end
}
puts my_object # --> 52, 60
puts other # --> 52, 60
other.reset
puts my_object # --> 52, 60
puts other # --> ,


No comments: