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_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 andmethod_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
2 comments:
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.
Post a Comment