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.


 

Sunday, February 19, 2006

The Way of Meta - Part II

The Way of Meta

V. 0.0.2

Exploring Ruby Metaprogramming Capabilities

 

Getting our Feet Wet

Tired of all this talk?  Then it is time to dip our feet in the deep waters of rubesque metaprogramming.  We are going to start our journey exploring the metaprogramming features that are illustrated in the pragmatic programmers' PickAxe book, while at the same time we will go through the less known libraries present in the ruby distribution.  At the same time we will ask for some help to the wonderfully brief and clear 'Seeing Metaclasses Clearly' by Why the Lucky Stiff.

 

Out-of-the-Box Metaprogramming Mechanisms

Ruby provides several straight out of the box metaprogramming mechanisms with its core standard libraries.  Although using these mechanisms can take you very quickly to unmanageable levels of complexity it is worth looking at them to have a feeling of what is possible.  Later on we will encapsulate these mechanisms within our own metaprogramming protocol.


Loading Code

In many languages, the mechanism that allows your source code to refer to a library or to another piece of code is handled by the language in an opaque way.  Not so in Ruby.

When using Ruby you are in charge of the code loading.

The 'load' command loads some ruby code, right where you are calling it.

 

load "myfile.rb"

 

The 'require' command adds a little check that prevents the loading of the same file twice from different points in the code.

 

require "filename"

 

Load and require are not special commands for the compiler, they are dynamically executed and can be embedded within blocks, control statements, etc.  The name of the file that they load can also be constructed dynamically.

The load construct in particular allows you to reload code definitions any number of times if they get changed inside the file.  It's now easy to see how your code could generate code files, maybe using a templating mechanism, and then load them back in the runtime.

 

Principles of Ruby Metaprogramming I: Source Code Files are Dynamically Loaded, not Statically Included

 

There are two environment variables that can turn out to be helpful when dealing with dynamic source code loading.

$LOAD_PATH is an array of directories that are searched for source code by 'load' and 'require'. 

$LOADED_FEATURES is an array containing the filenames of the files that have already been loaded.




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