Monday, January 19, 2009

Methodless Ruby


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...





TOC

Where 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 Problems
Some 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.

Codereuse
Being lazy I want mixins an that kind of stuff, right!

Résumé
What did I actually do? Where shall I go?





Back to TOC
Where 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 TOC
But 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 TOC
Fixing 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 Proc
Subclassing 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 Objects
In 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.

Criticisms
Although 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 TOC
Quinctilie 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 TOC
Code 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 TOC
Ré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.

2 comments:

tea42 said...

I'm curious to see how far you get in actually using something like this. I've played with the idea in the past too. Rather then #behavior I used #fn. And rather then use a hash I used an OpenObject (or OpenStruct).

Robert Dober said...

So am I, please stay tuned. I have a strong feeling that this will turn towards closures as I am influenced by David Thomas' keynote to Rubyconf.

Using open object was surely a nice idea.