In an earlier post I have shown how Ruby can be used (or abused) as a language implementing different object models than the built in model. Ruby's built in model is Single Inheritance and Module Mixin as probably everybody reading this knows. If you have stumbled upon this blog and do not know Ruby's object model you might want to read about it first. E.g. in the famous
Pickaxe Book.
The last time I was exploring object models I blogged about
"Classless Ruby".
I was however still making use of modules and methods.
Today I will get rid of them too, well I will still write some Core class methods but once that is done there will be no more classes, no more modules, no more methods and no more instance variables.
The outcome will resemble
prototypes, but the beauty of the whole, the beauty of Ruby, is that even that is just my implementation and different developement paradigms could be explored almost as easily.
'nuff ta'king for now...
TOCWhere To Put The Behavior? A very revolutionary idea stolen from Perl 5! (five not 120)
But What Is Behavior, Now? Hashes might still store proc objects for different purposes though.
Fixing Some ProblemsSome of the shortcomings discovered so far can be fixed...
Quinctilie Vare, legiones hash redde![1] I am much luckier than Quinctilius Varus, I can give the hash back.
CodereuseBeing lazy I want mixins an that kind of stuff, right!
Résumé What did I actually do? Where shall I go?
Back to TOCWhere To Put Behavior?
I did not really think a long time about this, and I do not believe that there are many alternatives with the given constraints. Hashes really should do the job nicely.
They allow us to store the data and the behavior together ( we are not abandoning the encapsulation principle yet ). As I mentioned above that is exactly what Larry Wall did in Perl 5 and that allowed for some nice object oriented programs after all.
A straightforward shot at this might look like the following
1 book = {
2 :author => 'Mark Twain',
3 :date => 2364,
4 :title => 'Tom Sawyer meets Data',
5 :image => proc{ | myself |
6 puts %<"#{myself[:title]}" by #{myself[:author]} was published in #{myself[:date]}>
7 }
8 }
9
10 book[:image].call( book )
11
When we look at line #10 however we are not very happy, are we? Well we should not. There are two redundancies involved. Firstly we have to pass the receiver into the proc object again, and secondly we have to call the proc object recursively. We will get rid of this nuisance immediately.
1 class Hash
2 alias_method :__old_get__, :[]
3 def [] name, *args
4 value = __old_get__( name )
5 return value unless value && value.is_a? Proc
6 value.call( self, *args )
7 end
8 end
and now this works
1 book[:image] # --> "Tom Sawyer meets Data" by Mark Twain was published in 2364
Back to TOCBut What Is Behavior, Now?
We are very, very happy with our design, we can pass arguments into our new
[]
call syntax as the following example shows:
1 book = {
2 :author => 'Mark Twain',
3 :date => 2364,
4 :title => 'Tom Sawyer meets Data',
5 :image => proc{ | myself |
6 puts %<"#{myself[:title]}" by #{myself[:author]} was published in #{myself[:date]}
7 annotations: #{(myself[:annotations]||[]).join(", ")}>
8 },
9 :add_annotations => proc{ |myself, *annotations|
10 myself[ :annotations ] ||= []
11 myself[ :annotations ] += annotations
12 }
13
14 }
15
16 book[:add_annotations, "Edited by J-L.Picard", "Reviewed by William T. Riker"]
17 book[:image]
and we get the hoped for
"Tom Sawyer meets Data" by Mark Twain was published in 2364
annotations: Edited by J-L.Picard, Reviewed by William T. Riker
Thus I am happy again, and often when I am happy, there is something really, really wrong. One of the things that are wrong became apparent when I needed to store a proc object in the hash and that proc object was not implementing behavior of the object.
1 book[:adder] = proc{ | a, b | a + b }
2 a = book[:adder]
When running this I got of course an error
method-less-4.rb:34:in `block in ': undefined method `+' for # (NoMethodError)
from method-less-4.rb:10:in `call'
from method-less-4.rb:10:in `[]'
from method-less-4.rb:35:in `'
Back to TOCFixing Some Problems
So far we have successfully bound behavior to data with procs. We have however introduced an ambiguity between
Behavior members and
Proc members in our objects.
Furthermore we have not yet introduced a generic object creation mechanism. No,
dup
will not suffice, but there is not needed much more.
Worse though, there is no object initialization mechanism either. We will fix these three issues now and then review what we have got, again.
Behavior is not ProcSubclassing Proc shall give us the information we need to do the
right thing when accessing an object's member. Furthermore a nice subclass factory method, e.g.
Kernel#behavior
will provide more readable code.
1
2 Behavior = Class::new Proc
3
4 module Kernel
5 def behavior &blk
6 Behavior::new( &blk )
7 end
8 end
9
In lines 1 to 8 we just subclass Proc and define a Kernel method that wraps the constructor call of Behavior which is the inherited one.
9
10 class Hash
11 alias_method :__old_get__, :[]
12 def [] name, *args
13 value = __old_get__( name )
14 return value unless value && value.is_a?( Behavior )
15 value.call( self, *args )
16 end
17 end
18
Thanks to our little sub-classing game before we can dispatch on Behavior now. By specifying Behavior with the
behavior
method, and Proces with the
proc
method (see lines 23, 27 & 31) we implement the desired behavior ( no pun intended ).
18
19 book = {
20 :author => 'Mark Twain',
21 :date => 2364,
22 :title => 'Tom Sawyer meets Data',
23 :image => behavior{ | myself |
24 puts %<"#{myself[:title]}" by #{myself[:author]} was published in #{myself[:date]}
25 annotations: #{(myself[:annotations]||[]).join(", ")}>
26 },
27 :add_annotations => behavior{ |myself, *annotations|
28 myself[ :annotations ] ||= []
29 myself[ :annotations ] += annotations
30 },
31 :adder => proc{ | a, b | a + b }
32 }
33
34 book[:add_annotations, "Edited by J-L.Picard", "Reviewed by William T. Riker"]
35 book[:image]
36
37
38 book[:adder] = proc{ | a, b | a + b }
39 a = book[:adder]
40 p a.call( 40, 2 )
and we get
"Tom Sawyer meets Data" by Mark Twain was published in 2364
annotations: Edited by J-L.Picard, Reviewed by William T. Riker
42
Constructing and Initializing ObjectsIn order to construct objects we have to make sure that they do not share any data with the
prototype. This holds even more as, in this kind of prototype programming
any object can be used as the receiver of the construction message.
1 class Hash
2 def new initial_values = {}
3 dup
4 initial_values.inject( dup ){ | o, (k, v) |
5 o.update k => ( v.dup rescue v )
6 }
7 end
8 end
In the code above we just define a
new
method for a hash. N.B. This is an instance method and must not be confused with Hashes class method. Maybe a different name would have been in order, I honestly do not know. We duplicate the receiver and inject the initial values hash. That can than be used as follows.
1 book = {
2 :image => behavior{ | myself |
3 puts %<"#{myself[:title]}" by #{myself[:author]} was published in #{myself[:date]}
4 annotations: #{(myself[:annotations]||[]).join(", ")}>
5 },
6 :add_annotations => behavior{ |myself, *annotations|
7 myself[ :annotations ] ||= []
8 myself[ :annotations ] += annotations
9 }
10 }
11
12 i_robot = book.new( :title => "I Robot", :author => "Isaac Asimov", :date => 1950 )
13 pickaxe = book.new( :title => "Programming Ruby 1.9", :author => "Dave Thomas", :date => "2009" )
14 pickaxe[:add_annotations, "Live Long and Ruby!", "and there was Andy Hunt", "... and Chad Fowler" ]
15
16 i_robot[ :image ]
17 pickaxe[ :image ]
which duly produces the following output:
"I Robot" by Isaac Asimov was published in 1950
annotations:
"Programming Ruby 1.9" by Dave Thomas was published in 2009
annotations: Live Long and Ruby!, and there was Andy Hunt, ... and Chad Fowler
Please also note that we could have written
pickaxe = i_robot.new(...)
in line #13 above, without any change in the semantics of the program.
Did I say Prototype?I have used the term
prototype rather nonchalantly so far. Maybe it is time to define a bit better what
I understand under this term in this context.
A
prototype is an object oriented model in which the behavior of any object can be shared by using this very object as a
prototype. This is a little more general than e.g. the Javascript
prototype model in which each object simply has a
prototype and this prototype cannot be used as a member of the set of objects it defines.
In our case however that is perfectly possible.
That is the reason why the above remark about line #13 holds: Either
book
or
i_robot
can be used as a prototype, although they are perfectly members of the set of objects defined by the prototype.
CriticismsAlthough I find it very amusing, amazing and instructive what I have done here I have to get my feet back on the ground again.
This design suffers from some serious drawbacks.
- All members are accessible.
- Calling behavior with blocks has a clumsy syntax (namely
send :[],... do end
).
- Name Errors (e.g. behavior names misspelled ) are completely obfuscated.
- There is one only constructor, it does not allow for different behavior depending on the prototype.
- Last but not least, we have monopolized hashes. And that of course is completely inacceptable.
The public accessibility of all members is somehow not really an issue in some application contexts. The clumsy syntax can be worked around and probably the naming problem can be resolved by a different implementation of
Hash#[]
e.g. using #fetch.
However, the last two points are inacceptable.
Back to TOCQuinctilie Vare, legiones hash redde!
Our first duty to the emperor Ruby is to give her her hashes back. I have lost them in the quest for new territory for the Ruby empire but luckily for me I can give them back (for a - however very little - prize ).
The solution of course is not to use instances of
Hash
as our
Object -- Behavior glue. Well actually we still will use hashes, but specialized hashes, a lession learned from our
Behavior
class. Well let's do it:
1
2 class BHash < Hash
3 def [] name, *args
4 value = super( name )
5 return value unless value && value.is_a?( Behavior )
6 value.call( self, *args )
7 end
8 def new *args
9 dup.tap do | o |
10 o[:init, *args] if o.fetch( :init, nil ).is_a? Behavior
11 end
12 end
13 end
14
First we have defined our subclass of
Hash
called
BHash
(for Behavior Hash). We can find the redefinition of the
#[]
method again and the generic constructor. Thus we have freed the core Hash class from all our
achievements. Please do note that some other issues could be handled here easily now, as e.g. raising a
NameError
for access to undefined keys. This would make the code even longer though and not be of much technical interest here.
14
15 class Hash
16 def to_bhash
17 BHash::new.update self
18 end
19 end
20
21 module Kernel
22 def object a_hash
23 a_hash.to_bhash
24 end
25 end
26
In the next block of code we have handled the issue I had mentioned above, the
small prize to pay for our sub-classing. This prize might even be a blessing in disguise because it frees the hash literal syntax, well for, hash literals.
Thus first I defined a convenient conversion method, that creates a
BHash
out from a
Hash
and then I provided a wrapper method in
Kernel
called
object
. This changes the look of file of our object definitions a little bit.
26
27 book = object :image => behavior{ | myself |
28 puts %<"#{myself[:title]}" by #{myself[:author]} was published in #{myself[:date]}
29 annotations: #{(myself[:annotations]||[]).join(", ")}>
30 },
31 :add_annotations => behavior{ | myself, *annotations |
32 myself[ :annotations ] ||= []
33 myself[ :annotations ] += annotations
34 },
35 :init => behavior{ | myself, title, others={} |
36 myself[ :title ] = title
37 myself.update others
38 }
39
40
41 gray = book.new( "The Picture of Dorian Gray", :author => "Oscar Wilde", :date => 1890 )
42 pickaxe = book.new( "Programming Ruby 1.9", :author => "Dave Thomas", :date => 2009 )
43 pickaxe[:add_annotations, "Live Long and Ruby!", "and there was Andy Hunt", "... and Chad Fowler" ]
44
45 gray[ :image ]
46 pickaxe[ :image ]
And this code is satisfying our conditions above and produces the expected output:
"The Picture of Dorian Gray" by Oscar Wilde was published in 1890
annotations:
"Programming Ruby 1.9" by Dave Thomas was published in 2009
annotations: Live Long and Ruby!, and there was Andy Hunt, ... and Chad Fowler
1 Behavior = Class::new Proc
2
3 module Kernel
4 def behavior &blk
5 Behavior::new( &blk )
6 end
7 def object a_hash
8 a_hash.to_bhash
9 end
10 end
11
12 class BHash < Hash
13 def [] name, *args
14 value = super( name )
15 return value unless value && value.is_a?( Behavior )
16 value.call( self, *args )
17 end
18 def new *args
19 dup.tap do | o |
20 o[:init, *args] if o.fetch( :init, nil ).is_a? Behavior
21 end
22 end
23 end
24
25 class Hash
26 def to_bhash
27 BHash::new.update self
28 end
29 end
30 book = object :image => behavior{ | myself |
31 puts %<"#{myself[:title]}" by #{myself[:author]} was published in #{myself[:date]}
32 annotations: #{(myself[:annotations]||[]).join(", ")}>
33 },
34 :add_annotations => behavior{ | myself, *annotations |
35 myself[ :annotations ] ||= []
36 myself[ :annotations ] += annotations
37 },
38 :init => behavior{ | myself, title, others={} |
39 myself[ :title ] = title
40 myself.update others
41 }
42
43
44 gray = book.new( "The Picture of Dorian Gray", :author => "Oscar Wilde", :date => 1890 )
45 pickaxe = book.new( "Programming Ruby 1.9", :author => "Dave Thomas", :date => 2009 )
46 pickaxe[:add_annotations, "Live Long and Ruby!", "and there was Andy Hunt", "... and Chad Fowler" ]
47
48 gray[ :image ]
49 pickaxe[ :image ]
50
Back to TOCCode Reuse
Well we are all lazy, it's almost a hype ;). Seriously there is one feature we might miss at first sight, that of mixins. On second sight of course and knowing that we use a subclass of Hash as our behavior and state container we see that we can simply update the container with different behavior. A naive, but perfectly working approach would be to do the following:
1 class BHash < Hash
2 def add_behavior behavior
3 abort "Not a behavior #{behavior}" unless
4 behavior.is_a? self.class # a tailormade exception would be raised in production code, of course
5 update behavior
6 end
7 end
Given this primitive wrapper around
Hash#update
we can now
extend our prototypes / objects as in the following code:
1 isbn = object :set_isbn => behavior { |myself, isbn | myself[ :isbn ] = myself[ :add_isdn_check, isbn ] },
2 :add_isdn_check => behavior { | _, isbn |
3 # begin of boring cksum code
4 isbn + "*"
5 # end of boring chksum code
6 }
7
8 book.add_behavior isbn
9 hegel = book.new( "Hegel's Philosophy of Reality", :author => "Robert M. Wallace", :date => 2005 )
10 hegel[ :set_isbn, "521844843" ]
11 hegel[ :image ]
12 puts hegel[ :isbn ]
13
printing
"Hegel's Philosophy of Reality" by Robert M. Wallace was published in 2005
annotations:
521844843*
Back to TOCRésumé
I have now implemented the basic devices needed for prototype based programming, and I am asking myself,
What now?. As a matter of fact I am looking back on the code I have written I am missing some properties badly:
- Call syntax is clumsy for blocks.
- Code reuse does not allow access to overridden behavior.
- Data and behavior are mixed.
- Data and behavior is public.
Now I am sure, that all of these matters can be fixed, but shall this be done with hashes as the basic device?
I am really not sure and there is just one way to find out, implementing some of it and playing around with it...
And that is what I will be doing now. Thanks for having stepped by.