Tuesday, February 21, 2006

The Way of Meta - Part III

The Way of Meta

~

V. 0.0.3

 

Writing Method Generators

I am going to write a simple example that illustrates how you can create a method generator: a construct used within a class definition as a shortcut to generate richer method semantics, in the same way that attr_accessor generates enrich class behaviour by adding new methods.

This example concerns the development of a 'synonym' generator.   A synonym is similar to an alias because it provides different calls to achieve the same result.  It is different from an alias in that an alias generates a copy of a method, rather than a different name to call the same method.

If an aliased method gets overridden, its aliases will keep calling the old code.   Conversely, a synonym of a method is a simple light code layer that keeps calling a certain method name.  If the aliased method gets overwritten, then all its synonyms will start calling the new version of the method.

Let's start from our intent; let's visualise how we would like to use synonyms.

class Person

    def say

        "hi"

    end    

 

    def walk

        "one step"

    end

 

    synonym      :say   => [ :talk ,   :discuss , :greet ],

                 :walk => [ :stride, :run,     :jump  ]

end

 

Here we are declaring that both the method 'say' and the method 'walk' have several synonyms.

We also expect the following statements to be true

p1 = Person.new

 

p p1.talk       == "hi"

p p1.discuss    == "hi"

p p1.greet      == "hi"

 

p p1.stride == "one step"

p p1.run    == "one step"

p p1.jump   == "one step"

 

They should become false when we override the old methods:

class Person

    def say

        "yo"

    end

end

p p1.say        == "yo"

p p1.talk       == "yo"

p p1.discuss    == "yo"

p p1.greet      == "yo"

 

Enough talking, we have all the intent we need for a first stab at the problem.   Let's dive straight into the code.

First of all we need a convenient way to process the arguments passed to the 'synonym' generator.

Consider how we will need to elaborate the following hash:

    :say   => [ :talk,   :discuss, :greet ],

    :walk => [ :stride , :run ,     :jump   ]

 

This is just a convenient idiom that we use to express the following relationships:

    [

    [ :say,  :talk   ],

    [ :say,  :discuss],

    [ :say,  :greet  ],

    [ :walk, :stride ],

    [ :walk, :run    ],

    [ :walk, :jump   ]

]

 

Let's write some code that can take us from the first compact format to the second explicit representation.   We will call the relationships 'associations' and we will provide a method that allows us to iterate over a hash one association at a time.

# expands a => [b1,b2,b3] associations to [a,b1], [a,b2], [a,b3], etc..

class Hash

    def each_association &block

        self .each_pair do | assoc_key, assoc_targets|

            assoc_targets.to_a.each do |assoc_target|

                block.call assoc_key, assoc_target

            end

        end

    end   

end

 

Now that we have a way to visit our associations, we will generate a new synonym method for each association:

class Module

    # associations: ( old_name => [new names] )*

    def synonym associations

        associations.each_association do

            | original_name_sym, new_name_sym|

           

            define_method( new_name_sym){ |* args|

                self .send original_name_sym, *args

            }

           

        end

    end

   

end

 

For each association specified in the synonym generator we inject a new method in our class.   The new method has the name specified on the right hand side of the association, and all it does is to invoke the original method of the class.

The synonym generator has been defined within the Module class, so that it becomes automatically available within the context of the classes 'Module' and 'Class'.

We can now use the generator even to expand existing classes with new developer friendly names.  

Here we tweak the Array class:

Array.class_eval do

    synonym :size => [ :count, :n_elements ]

end

 

p [1, 2,3 ].count         == 3

p [1, 2,3 ].n_elements    == 3

 

And here we provide a way to talk to a Person, rather than giving him orders!

class Person

    def calculate( a,op,b)

        a.send op, b

    end

end

 

p (p1.calculate 3, :+, 2) == 5

 

class Person

    synonym :calculate => :how_much ?

end

 

p (p1.how_much? 3, :+, 2) == 5

 

It is worth pointing out a small syntactic trick that we used to handle both cases of single ( :calculate => :how_much ?) and multiple synonyms (:size => [ :count , :n_elements ]).

 

We get the right hand side of the hash element and we apply 'to_a' to it, turning it into an array.

            assoc_targets.to_a.each do |assoc_target|

            end

 

If the element is not an array, it gets turned into one.   If it is already an array, it is left unchanged.  Whatever the case, we are left with a simple array to operate on in the end.


 

Comments:
Thankyou for the nice exposition. I just ran some of this code in Ruby 1.8.3 and it threw a warning on using 'to_a' on a symbol, saying it will be obsolete.

Also, you could use the 'alias' method directly, instead of having to define a new method. Replace the 'define_method' call with module_eval "alias #{new_name_sym} #{original_name_sym}"

Ruby rocks!
 
Thanks to you for taking the time to read and to comment. I'm running the code on Ruby 1.8.4 and define_method is legal there. I didn't go the 'alias' way because alias is cloning a method, whereas I wanted to have all new methods pointing to the original method. This is useful when the definition of the base method changes. All the synonyms get automatically alignes, while the aliases don't. I'll have a look at to_a. I keep the warnings off by defult because I don't like using parenthesis.
 
I like where you are going, but I don't think modifying the Hash built-in is the way I would choose to meta-program the synonym example. Why not just use a class method that defines methods for each word and references a hash? Or use method_missing and just lookup the words...
 
modifying the hash was just utility code to make the rest of the code more expressive. I didn't need to do it, I just liked to have a nice method to explode pairs of associations.

For what concerns method_missing I have used it a lot in the past, but I think it tends to make errors harder to find and I always try to generate a method rather than doing dynamic dispatching via method-missing.

However there are a few interesting method-missing tricks I'll put in a future post.
 
Post a Comment



<< Home

This page is powered by Blogger. Isn't yours?