The best kittens, technology, and video games blog in the world.

Sunday, October 29, 2006

Prototype-based Ruby

Guardians of the horizon by good day from flickr (CC-NC)
Object-oriented programming is all about objects. About objects and messages they pass to each other.

Programs contain many objects. Way too many to make each of them by hand. Objects need to be mass-produced. There are basically two ways of mass producing objects.

  • The industrial way - building factory objects that build other objects.
  • The biological way - building prototype objects that can be cloned.
A language with classes that are not objects is not object-oriented. Period.

Most object-oriented languages like Smalltalk and Ruby use the industrial way. The object factories are also known as "class objects" (or even "classes", but that's a bit confusing).

To create a new object factory you do:
a_factory = Class.new()
a_factory.define_instance_method(:hello) {|arg|
puts "Hello, #{arg}!"
}
And then:
an_object = a_factory.new()
an_object.hello("world")
Some languages like Self and The Most Underappreciated Programming Language Ever (TMUPLE, also known as JavaScript), use biological method instead. In biological method you create a prototype, then clone it:
a_prototype = Object.new()
a_prototype.define_method(:hello) {|arg|
puts "Hello, #{arg}!"
}
Then:
an_object = a_prototype.clone()
an_object.hello("world")
Biological way is less organized, but simpler and more lightweight. There are only objects and messages, nothing more. Array is a prototype for all arrays and so on.

Industrial way is more organized, but much more complex and heavy. There are objects, classes, class objects, superclasses, inheritance, mixins, metaclasses, singleton classes. It's just too complex.

This complexity exists for a reason, but sometimes we'd really rather get away with it and use something simpler.

Prototype-based programming in Ruby

And in Ruby we can !

First, we need to be able to define methods just for individual objects:
def x.hello(arg)
puts "Hello, #{arg}!"
end

x.hello("world") # => "Hello, world!"
Now we just need to copy existing objects:
y = x.clone()
y.hello("world") # => "Hello, world!"
The objects are independent, so each of them can redefine methods without worrying about everyone else:
z = x.clone()

def x.hello(arg)
puts "Guten Tag, #{arg}!"
end

def z.hello(arg)
puts "Goodbye, #{arg}!"
end

x.hello("world") # => "Guten Tag, world!"
y.hello("world") # => "Hello, world!"
z.hello("world") # => "Goodbye, world!"
Converting class objects into prototype objects would probably introduce compatibility issues, so let's go halfway there:
class Class
def prototype
@prototype = new unless @prototype
return @prototype
end
def clone
prototype.clone
end
end

def (String.prototype).hello
puts "Hello, #{self}!"
end

a_string = String.clone
a_string[0..-1] = "world"

a_string.hello #=> "Hello, world!"

Horizontal gene transfer

Of course transfer of genes from parents to offspring is only half of the story. The other half is gene transfer between unrelated organisms.

We can easily use delegation and method_missing, but let's do something more fun instead - directly copying genes (methods) between objects.

a_person = Object.new
class <<a_person
attr_accessor :first_name
attr_accessor :name

def to_s
"#{first_name} #{name}"
end
end

nancy_cartwright = a_person.clone
nancy_cartwright.first_name = "Nancy"
nancy_cartwright.name = "Cartwright"

hayashibara_megumi = a_person.clone
hayashibara_megumi.first_name = "Megumi"
hayashibara_megumi.name = "Hayashibara"
But Megumi is Japanese, so she needs reversed to_s method:

def hayashibara_megumi.to_s
"#{name} #{first_name}"
end
Later we find out that another person needs reversed to_s:
inoue_kikuko = a_person.clone
inoue_kikuko.first_name = "Kikuko"
inoue_kikuko.name = "Inoue"
We want to do something like:
japanese_to_s = hayashibara_megumi.copy_gene(:to_s)
inoue_kikuko.use_gene japanese_to_s
OK, first let's fix a few deficiencies of Ruby 1.8.
define_method is private (should be public), and
there is no simple singleton_class.
Both will hopefully be fixed in Ruby 2.

class Object
def singleton_class
(class <<self; self; end)
end
end

class Class
public :define_method
end
And now:
class Object
def copy_gene(method_name)
[method(method_name).unbind, method_name]
end

def use_gene(gene, new_method_name = nil)
singleton_class.define_method(new_method_name||gene[1], gene[0])
end
end
We can try how the gene splicing worked:
puts nancy_cartwright #=> Nancy Cartwright 
puts hayashibara_megumi #=> Hayashibara Megumi
puts inoue_kikuko #=> in `to_s':TypeError: singleton method called for a different object
If we try it in Ruby 1.9 we get a different error message:
puts inoue_kikuko #=> in `define_method': can't bind singleton method to a different class (TypeError)
What Ruby does makes some sense - if method was implemented in C (like a lot of standard Ruby methods), calling it on object of completely different "kind" can get us a segmentation fault. With C you can never be sure, but it's reasonably safe to assume that we can move methods between objects with the same "internal representation".

We need to use Evil Ruby. Evil Ruby lets us access Ruby internals.
UnboundMethod class represents methods not bound to any objects. It contains internal field rklass, and it can only bind to objects of such class (or subclasses). First, let's define a method to change this rklass:

class UnboundMethod
def rklass=(c)
RubyInternal.critical {
i = RubyInternal::DMethod.new(internal.data)
i.rklass = c.object_id * 2
}
end
end
Now we could completely remove protection, but we just want to loosen it. Instead of classes, we want to compare internal types:
class Object
def copy_gene(method_name)
[method(method_name).unbind, method_name, internal_type]
end

def use_gene(gene, new_method_name = nil)
raise TypeError, "can't bind method to an object of different internal type" if internal_type != gene[2]
gene[0].rklass = self.class
singleton_class.define_method(new_method_name||gene[1], gene[0])
end
end
And voilà!
puts nancy_cartwright #=> Nancy Cartwright 
puts hayashibara_megumi #=> Hayashibara Megumi
puts inoue_kikuko #=> Inoue Kikuko
This is merely a toy example. But sometimes prototypes lead to design more elegant than factories. Think about the possibility in your next project.

Full listing

require 'evil'

class Object
def singleton_class
(class <<self; self; end)
end
end

class UnboundMethod
def rklass=(c)
RubyInternal.critical {
i = RubyInternal::DMethod.new(internal.data)
i.rklass = c.object_id * 2
}
end
end

class Class
public :define_method
end

class Object
def copy_gene(method_name)
[method(method_name).unbind, method_name, internal_type]
end

def use_gene(gene, new_method_name = nil)
raise TypeError, "can't bind method to an object of different internal type" if internal_type != gene[2]
gene[0].rklass = self.class
singleton_class.define_method(new_method_name||gene[1], gene[0])
end
end

a_person = Object.new
class <<a_person
attr_accessor :first_name
attr_accessor :name

def to_s
"#{first_name} #{name}"
end
end

nancy_cartwright = a_person.clone
nancy_cartwright.first_name = "Nancy"
nancy_cartwright.name = "Cartwright"

hayashibara_megumi = a_person.clone
hayashibara_megumi.first_name = "Megumi"
hayashibara_megumi.name = "Hayashibara"

def hayashibara_megumi.to_s
"#{name} #{first_name}"
end

inoue_kikuko = a_person.clone
inoue_kikuko.first_name = "Kikuko"
inoue_kikuko.name = "Inoue"

japanese_to_s = hayashibara_megumi.copy_gene(:to_s)
inoue_kikuko.use_gene japanese_to_s

puts nancy_cartwright
puts hayashibara_megumi
puts inoue_kikuko

3 comments:

Brian Mitchell said...

Nice post. I've posted a comment on reddit showing an alternative method.

mfp said...

Is evil.rb working OK for you?
I had to change the way it obtained the VALUE from the object_id earlier this year (something changed in Ruby and object ids became negative around 2005).

That was around the time I wrote
Tricking that old, picky interpreter: prototype-based OOP, similar in intent (and complementary) to your code.

BTW, I was going to write
"Nancy Cartwright? Who would choose not to listen to the Japanese audio track?" before I realized that you were probably referring to her role as Bart Simpson (she doesn't do anime according to imdb). I've watched the Simpsons in Spanish, French & German, but only a few episodes in English :)

/me doesn't want to imagine anybody trying to impersonate Inoue's Belldandy.

Anonymous said...
This comment has been removed by a blog administrator.